From 83bb6385b72520b59c01adebd83962e14565f103 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:00:20 +0000 Subject: [PATCH 01/75] fix: missing .env file is raising extra warnings in Sentry during dev. .env is already in .gitignore, any local changes made should not be tracked. Reverts 11a0cd 661ff2. --- .env | 3 +++ tests/conftest.py | 2 ++ tilia/boot.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 000000000..e88594eb0 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +LOG_REQUESTS = 1 +EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE +ENVIRONMENT='dev' diff --git a/tests/conftest.py b/tests/conftest.py index 24ab48ce5..77eaab4a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,8 @@ dotenv_path = PROJECT_ROOT / ".env" success = dotenv.load_dotenv(dotenv_path) +if not success: + raise FileNotFoundError(f"No .env file found at {dotenv_path.resolve()}") class TiliaErrors: diff --git a/tilia/boot.py b/tilia/boot.py index 5fbf2afe0..8a98fbd3f 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -38,7 +38,7 @@ def boot(): dotenv_path = PROJECT_ROOT / ".env" success = dotenv.load_dotenv(dotenv_path) if not success: - print(f"Could not load environment variables from {dotenv_path}") + raise FileNotFoundError(f"No .env file found at {dotenv_path.resolve()}") args = setup_parser() setup_dirs() logger.setup() From 7c189f0ce2c3e79817070ddd0c3d6e37e5751438 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:04:17 +0000 Subject: [PATCH 02/75] ci: test for python 3.13 removal of pydub (5c132b) and related audioop should allow 3.13 to compile. --- .github/workflows/run-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e69a3fc6e..5acebd1ac 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,8 +24,7 @@ jobs: strategy: fail-fast: false matrix: - # TODO: test for 3.13 - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [macos-latest, ubuntu-latest, windows-latest] include: - os: ubuntu-latest From 9d764f51d625857c5c6728556b04282738bb3c02 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:10:52 +0100 Subject: [PATCH 03/75] ci: run tests for newer python versions --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5acebd1ac..ada19a0c2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] os: [macos-latest, ubuntu-latest, windows-latest] include: - os: ubuntu-latest From b89f9db0e6c41cd8f2e53ad836e29ee496270d81 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:36:30 +0000 Subject: [PATCH 04/75] fix: missing objectName --- tilia/ui/windows/inspect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tilia/ui/windows/inspect.py b/tilia/ui/windows/inspect.py index 0ba467f31..5c1afd3ad 100644 --- a/tilia/ui/windows/inspect.py +++ b/tilia/ui/windows/inspect.py @@ -49,6 +49,7 @@ class Inspect(QDockWidget): def __init__(self, main_window) -> None: super().__init__(main_window) + self.setObjectName("inspector") self.setWindowTitle("Inspector") self.setMinimumWidth(250) self.setFeatures( From d6a5b84bc1eb74d90009a85ff3d3131abdbc5ac5 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:17:07 +0100 Subject: [PATCH 05/75] build: PyQt -> PySide --- README.md | 2 +- requirements.txt | 8 +++----- setup.cfg | 8 +++----- tests/conftest.py | 4 ++-- tests/mock.py | 2 +- tests/parsers/csv/test_markers_from_csv.py | 2 +- tests/parsers/score/test_score_from_musicxml.py | 2 +- tests/timelines/base/test_validators.py | 2 +- .../hierarchy/test_hierarchy_timeline_ui.py | 2 +- tests/ui/timelines/interact.py | 6 +++--- .../timelines/marker/test_marker_timeline_ui.py | 6 +++--- tests/ui/timelines/marker/test_marker_ui.py | 4 ++-- tests/ui/windows/test_manage_timelines.py | 4 ++-- tests/utils.py | 2 +- tilia/boot.py | 2 +- tilia/media/player/base.py | 2 +- tilia/media/player/qtplayer.py | 4 ++-- tilia/media/player/qtvideo.py | 4 ++-- tilia/media/player/youtube.py | 16 ++++++++-------- tilia/parsers/score/musicxml_to_svg.py | 14 +++++++------- tilia/settings.py | 2 +- tilia/timelines/base/validators.py | 2 +- tilia/ui/actions.py | 0 tilia/ui/color.py | 2 +- tilia/ui/commands.py | 4 ++-- tilia/ui/dialogs/add_timeline_without_media.py | 4 ++-- tilia/ui/dialogs/basic.py | 4 ++-- tilia/ui/dialogs/by_time_or_by_measure.py | 4 ++-- tilia/ui/dialogs/choose.py | 2 +- tilia/ui/dialogs/crash.py | 6 +++--- tilia/ui/dialogs/file.py | 6 +++--- tilia/ui/dialogs/harmony_params.py | 2 +- tilia/ui/dialogs/mode_params.py | 2 +- tilia/ui/dialogs/resize_rect.py | 2 +- tilia/ui/menubar.py | 2 +- tilia/ui/menus.py | 4 ++-- tilia/ui/options_toolbar.py | 4 ++-- tilia/ui/player.py | 6 +++--- tilia/ui/qtui.py | 8 ++++---- tilia/ui/smooth_scroll.py | 10 +++++----- tilia/ui/timelines/audiowave/element.py | 6 +++--- tilia/ui/timelines/base/element.py | 4 ++-- tilia/ui/timelines/base/element_manager.py | 2 +- tilia/ui/timelines/base/timeline.py | 4 ++-- tilia/ui/timelines/beat/element.py | 9 +++++---- tilia/ui/timelines/collection/collection.py | 4 ++-- tilia/ui/timelines/collection/scene.py | 2 +- tilia/ui/timelines/collection/view.py | 4 ++-- tilia/ui/timelines/cursors.py | 2 +- tilia/ui/timelines/harmony/elements/harmony.py | 6 +++--- tilia/ui/timelines/harmony/elements/mode.py | 6 +++--- tilia/ui/timelines/harmony/level_label.py | 6 +++--- tilia/ui/timelines/hierarchy/element.py | 6 +++--- tilia/ui/timelines/hierarchy/handles.py | 6 +++--- tilia/ui/timelines/marker/element.py | 6 +++--- tilia/ui/timelines/pdf/element.py | 6 +++--- tilia/ui/timelines/pdf/timeline.py | 6 +++--- tilia/ui/timelines/scene.py | 6 +++--- tilia/ui/timelines/score/element/barline.py | 4 ++-- tilia/ui/timelines/score/element/clef.py | 6 +++--- .../ui/timelines/score/element/key_signature.py | 6 +++--- .../timelines/score/element/note/accidental.py | 6 +++--- tilia/ui/timelines/score/element/note/body.py | 6 +++--- .../timelines/score/element/note/ledger_line.py | 6 +++--- tilia/ui/timelines/score/element/note/ui.py | 2 +- tilia/ui/timelines/score/element/staff.py | 6 +++--- .../ui/timelines/score/element/time_signature.py | 6 +++--- tilia/ui/timelines/score/timeline.py | 6 +++--- tilia/ui/timelines/selection_box.py | 6 +++--- tilia/ui/timelines/slider/timeline.py | 6 +++--- tilia/ui/timelines/toolbar.py | 4 ++-- tilia/ui/timelines/view.py | 6 +++--- tilia/ui/windows/about.py | 4 ++-- tilia/ui/windows/beat_pattern.py | 2 +- tilia/ui/windows/fill_beat_timeline.py | 4 ++-- tilia/ui/windows/inspect.py | 4 ++-- tilia/ui/windows/manage_timelines.py | 6 +++--- tilia/ui/windows/metadata.py | 2 +- tilia/ui/windows/metadata_edit_fields.py | 2 +- tilia/ui/windows/metadata_edit_notes.py | 2 +- tilia/ui/windows/settings.py | 4 ++-- tilia/ui/windows/svg_viewer.py | 10 +++++----- tilia/ui/windows/view_window.py | 4 ++-- 83 files changed, 190 insertions(+), 193 deletions(-) create mode 100644 tilia/ui/actions.py diff --git a/README.md b/README.md index f19f9dbcd..0f268a3f1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ drawing

-TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured but easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PyQt library for its GUI. +TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured but easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI. TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned. diff --git a/requirements.txt b/requirements.txt index 6b0d12491..ba82e100c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,9 @@ prettytable>=3.7.0 lxml>=5.3.0 music21>=9.1 numpy~=1.26.4 -PyQt6==6.6.0 -PyQt6-Qt6==6.6.0 -PyQt6-sip==13.6.0 -PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.0 +PySide6>=6.9.0 +PySide6_Addons>=6.9.0 +PySide6_Essentials>=6.9.0 python-dotenv>=1.0.1 pypdf>=6.0.0 httpx~=0.28.1 diff --git a/setup.cfg b/setup.cfg index dc55445e6..4e1911938 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,11 +12,9 @@ install_requires = prettytable>=3.7.0 music21>=9.1 numpy~=1.26.4 - PyQt6==6.6.0 - PyQt6-Qt6==6.6.0 - PyQt6-sip==13.6.0 - PyQt6-WebEngine==6.6.0 - PyQt6-WebEngine-Qt6==6.6.0 + PySide6>=6.10.0 + PySide6_Addons>=6.10.0 + PySide6_Essentials>=6.10.0 python-dotenv>=1.0.1 pypdf~=4.2.0 sentry-sdk>=2.21.0 diff --git a/tests/conftest.py b/tests/conftest.py index 77eaab4a4..ff4838888 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,8 @@ import dotenv import pytest -from PyQt6.QtCore import QSettings -from PyQt6.QtWidgets import QApplication +from PySide6.QtCore import QSettings +from PySide6.QtWidgets import QApplication from colorama import Fore, Style import tilia.constants as constants_module diff --git a/tests/mock.py b/tests/mock.py index af1589495..54c1529f9 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -2,7 +2,7 @@ from typing import Any, Sequence from unittest.mock import patch, Mock -from PyQt6.QtWidgets import QFileDialog, QMessageBox, QInputDialog +from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog from tilia.requests import Get, Post, serve, server, stop_serving from tilia.requests import post as post_original diff --git a/tests/parsers/csv/test_markers_from_csv.py b/tests/parsers/csv/test_markers_from_csv.py index 722d8a4a9..9a12d13d3 100644 --- a/tests/parsers/csv/test_markers_from_csv.py +++ b/tests/parsers/csv/test_markers_from_csv.py @@ -3,7 +3,7 @@ from typing import Literal from unittest.mock import patch, mock_open -from PyQt6.QtWidgets import QFileDialog +from PySide6.QtWidgets import QFileDialog from tests.utils import undoable from tilia.requests import Post, post diff --git a/tests/parsers/score/test_score_from_musicxml.py b/tests/parsers/score/test_score_from_musicxml.py index b63fc4ed6..f4cae4fdf 100644 --- a/tests/parsers/score/test_score_from_musicxml.py +++ b/tests/parsers/score/test_score_from_musicxml.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from PyQt6.QtWidgets import QMessageBox +from PySide6.QtWidgets import QMessageBox from tilia.parsers.score.musicxml import notes_from_musicXML from tilia.timelines.component_kinds import ComponentKind diff --git a/tests/timelines/base/test_validators.py b/tests/timelines/base/test_validators.py index f38af83a1..3a41e574c 100644 --- a/tests/timelines/base/test_validators.py +++ b/tests/timelines/base/test_validators.py @@ -1,4 +1,4 @@ -from PyQt6.QtGui import QColor +from PySide6.QtGui import QColor from tilia.timelines.base.validators import validate_color diff --git a/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py b/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py index ca11860e8..2c48a54b8 100644 --- a/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py +++ b/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py @@ -1,5 +1,5 @@ import pytest -from PyQt6.QtGui import QColor +from PySide6.QtGui import QColor from tests.mock import Serve, patch_yes_or_no_dialog from tilia.requests import Post, Get, post diff --git a/tests/ui/timelines/interact.py b/tests/ui/timelines/interact.py index 09ed9cbe3..0c4e72095 100644 --- a/tests/ui/timelines/interact.py +++ b/tests/ui/timelines/interact.py @@ -1,8 +1,8 @@ from typing import Literal -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QGraphicsView, QGraphicsItem, QApplication -from PyQt6.QtTest import QTest +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QGraphicsView, QGraphicsItem, QApplication +from PySide6.QtTest import QTest from tilia.requests import Post, post, get, Get from tilia.ui.coords import time_x_converter diff --git a/tests/ui/timelines/marker/test_marker_timeline_ui.py b/tests/ui/timelines/marker/test_marker_timeline_ui.py index 17faa1b15..b8160fc31 100644 --- a/tests/ui/timelines/marker/test_marker_timeline_ui.py +++ b/tests/ui/timelines/marker/test_marker_timeline_ui.py @@ -1,8 +1,8 @@ from unittest.mock import patch -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QColor -from PyQt6.QtWidgets import QColorDialog, QInputDialog +from PySide6.QtCore import Qt +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QColorDialog, QInputDialog from tests.mock import Serve, patch_ask_for_string_dialog, patch_yes_or_no_dialog from tests.ui.test_qtui import get_toolbars_of_class diff --git a/tests/ui/timelines/marker/test_marker_ui.py b/tests/ui/timelines/marker/test_marker_ui.py index ea649e284..8cbe1eeed 100644 --- a/tests/ui/timelines/marker/test_marker_ui.py +++ b/tests/ui/timelines/marker/test_marker_ui.py @@ -1,7 +1,7 @@ from unittest.mock import patch, Mock -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QColor +from PySide6.QtCore import Qt +from PySide6.QtGui import QColor from tests.mock import PatchPost from tilia.requests import Post diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py index 2ee34c3a8..dbba21621 100644 --- a/tests/ui/windows/test_manage_timelines.py +++ b/tests/ui/windows/test_manage_timelines.py @@ -1,8 +1,8 @@ from typing import Literal import pytest -from PyQt6.QtCore import Qt -from PyQt6.QtTest import QTest +from PySide6.QtCore import Qt +from PySide6.QtTest import QTest from tests.mock import Serve from tilia.requests import Get, get diff --git a/tests/utils.py b/tests/utils.py index e5c29edff..9ff041d82 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,7 @@ from pprint import pformat from typing import Callable -from PyQt6.QtWidgets import QMenu +from PySide6.QtWidgets import QMenu from tilia.requests import get, Get, Post, post from tests.mock import patch_file_dialog diff --git a/tilia/boot.py b/tilia/boot.py index 8a98fbd3f..5c392e552 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -4,7 +4,7 @@ import traceback import dotenv -from PyQt6.QtWidgets import QApplication +from PySide6.QtWidgets import QApplication from tilia.app import App from tilia.clipboard import Clipboard diff --git a/tilia/media/player/base.py b/tilia/media/player/base.py index b3bf688b0..c4ee053a9 100644 --- a/tilia/media/player/base.py +++ b/tilia/media/player/base.py @@ -5,7 +5,7 @@ from enum import Enum, auto from pathlib import Path -from PyQt6.QtCore import QTimer +from PySide6.QtCore import QTimer import tilia.errors from tilia.media import exporter diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py index 2d986628c..bb4659c1b 100644 --- a/tilia/media/player/qtplayer.py +++ b/tilia/media/player/qtplayer.py @@ -2,8 +2,8 @@ import time -from PyQt6.QtCore import QUrl -from PyQt6.QtMultimedia import ( +from PySide6.QtCore import QUrl +from PySide6.QtMultimedia import ( QMediaPlayer, QAudioOutput, QAudio, diff --git a/tilia/media/player/qtvideo.py b/tilia/media/player/qtvideo.py index 08017396a..3c3c252ea 100644 --- a/tilia/media/player/qtvideo.py +++ b/tilia/media/player/qtvideo.py @@ -1,7 +1,7 @@ from __future__ import annotations -from PyQt6.QtMultimediaWidgets import QVideoWidget -from PyQt6.QtWidgets import QSizePolicy +from PySide6.QtMultimediaWidgets import QVideoWidget +from PySide6.QtWidgets import QSizePolicy from .qtplayer import QtPlayer from tilia.ui.windows.view_window import ViewWindow diff --git a/tilia/media/player/youtube.py b/tilia/media/player/youtube.py index a467cf1d4..c14688dd7 100644 --- a/tilia/media/player/youtube.py +++ b/tilia/media/player/youtube.py @@ -2,16 +2,16 @@ from enum import Enum from pathlib import Path -from PyQt6.QtCore import QUrl, pyqtSlot, QObject, QTimer, QByteArray -from PyQt6.QtWebChannel import QWebChannel +from PySide6.QtCore import QUrl, Slot, QObject, QTimer, QByteArray +from PySide6.QtWebChannel import QWebChannel import tilia.constants import tilia.errors from tilia.media.player import Player -from PyQt6.QtWebEngineWidgets import QWebEngineView -from PyQt6.QtWebEngineCore import QWebEngineSettings, QWebEngineUrlRequestInterceptor +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEngineUrlRequestInterceptor from tilia.media.player.base import MediaTimeChangeReason from tilia.requests import Post, post @@ -38,11 +38,11 @@ def __init__( self.player_toolbar_enabled = False self.display_error = display_error - @pyqtSlot("float") + @Slot("float") def on_new_time(self, time): self.set_current_time(time) - @pyqtSlot("int") + @Slot("int") def on_player_state_change(self, state): if state == self.State.UNSTARTED.value: post(Post.PLAYER_UPDATE_CONTROLS, PlayerStatus.WAITING_FOR_YOUTUBE) @@ -56,11 +56,11 @@ def on_player_state_change(self, state): else: self.set_is_playing(False) - @pyqtSlot("float") + @Slot("float") def on_set_playback_rate(self, playback_rate: float): self.set_playback_rate(playback_rate) - @pyqtSlot(str) + @Slot(str) def on_error(self, message: str) -> None: self.display_error(message) diff --git a/tilia/parsers/score/musicxml_to_svg.py b/tilia/parsers/score/musicxml_to_svg.py index 21289b173..0a565f8e3 100644 --- a/tilia/parsers/score/musicxml_to_svg.py +++ b/tilia/parsers/score/musicxml_to_svg.py @@ -4,14 +4,14 @@ from pathlib import Path from re import sub -from PyQt6.QtCore import ( - pyqtSlot, +from PySide6.QtCore import ( + Slot, QObject, QUrl, ) -from PyQt6.QtWebChannel import QWebChannel -from PyQt6.QtWebEngineCore import QWebEngineSettings -from PyQt6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtWebChannel import QWebChannel +from PySide6.QtWebEngineCore import QWebEngineSettings +from PySide6.QtWebEngineWidgets import QWebEngineView from tilia.requests import ( get, Get, @@ -26,11 +26,11 @@ def __init__(self, page, on_svg_loaded, display_error) -> None: self.on_svg_loaded = on_svg_loaded self.display_error = display_error - @pyqtSlot(str) + @Slot(str) def set_svg(self, svg: str) -> None: self.on_svg_loaded(svg) - @pyqtSlot(str) + @Slot(str) def on_error(self, message: str) -> None: self.display_error(message) diff --git a/tilia/settings.py b/tilia/settings.py index 9c143b224..3d346bfc0 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -1,4 +1,4 @@ -from PyQt6.QtCore import QSettings, QObject +from PySide6.QtCore import QSettings, QObject from pathlib import Path import tilia.constants diff --git a/tilia/timelines/base/validators.py b/tilia/timelines/base/validators.py index 5edf87d8a..fc36e080a 100644 --- a/tilia/timelines/base/validators.py +++ b/tilia/timelines/base/validators.py @@ -1,7 +1,7 @@ import math from typing import Any -from PyQt6.QtGui import QColor +from PySide6.QtGui import QColor def validate_time(value): diff --git a/tilia/ui/actions.py b/tilia/ui/actions.py new file mode 100644 index 000000000..e69de29bb diff --git a/tilia/ui/color.py b/tilia/ui/color.py index 13af1443c..9f4b5702d 100644 --- a/tilia/ui/color.py +++ b/tilia/ui/color.py @@ -1,4 +1,4 @@ -from PyQt6.QtGui import QColor +from PySide6.QtGui import QColor def get_tinted_color(color: str, factor: int) -> str: diff --git a/tilia/ui/commands.py b/tilia/ui/commands.py index b95f6215e..9e5e0a48f 100644 --- a/tilia/ui/commands.py +++ b/tilia/ui/commands.py @@ -37,8 +37,8 @@ import tilia.errors from tilia.dirs import IMG_DIR -from PyQt6.QtWidgets import QMainWindow, QWidget -from PyQt6.QtGui import QAction, QKeySequence, QIcon +from PySide6.QtWidgets import QMainWindow, QWidget +from PySide6.QtGui import QAction, QKeySequence, QIcon class CommandQAction(QAction): diff --git a/tilia/ui/dialogs/add_timeline_without_media.py b/tilia/ui/dialogs/add_timeline_without_media.py index c93c627a9..de79665af 100644 --- a/tilia/ui/dialogs/add_timeline_without_media.py +++ b/tilia/ui/dialogs/add_timeline_without_media.py @@ -1,7 +1,7 @@ from enum import Enum -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QRadioButton, diff --git a/tilia/ui/dialogs/basic.py b/tilia/ui/dialogs/basic.py index 8549f5ba4..eef71ba19 100644 --- a/tilia/ui/dialogs/basic.py +++ b/tilia/ui/dialogs/basic.py @@ -1,5 +1,5 @@ -from PyQt6.QtGui import QColor -from PyQt6.QtWidgets import QColorDialog, QInputDialog, QMessageBox +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QColorDialog, QInputDialog, QMessageBox def ask_for_color( diff --git a/tilia/ui/dialogs/by_time_or_by_measure.py b/tilia/ui/dialogs/by_time_or_by_measure.py index b95f2d4e8..388b1ff1a 100644 --- a/tilia/ui/dialogs/by_time_or_by_measure.py +++ b/tilia/ui/dialogs/by_time_or_by_measure.py @@ -1,5 +1,5 @@ -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QDialog, QLabel, QRadioButton, diff --git a/tilia/ui/dialogs/choose.py b/tilia/ui/dialogs/choose.py index d141eb20d..b7fa97cf1 100644 --- a/tilia/ui/dialogs/choose.py +++ b/tilia/ui/dialogs/choose.py @@ -1,6 +1,6 @@ from typing import Any -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDialog, QMainWindow, QLabel, diff --git a/tilia/ui/dialogs/crash.py b/tilia/ui/dialogs/crash.py index 69f61fc6b..3446c205b 100644 --- a/tilia/ui/dialogs/crash.py +++ b/tilia/ui/dialogs/crash.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import Qt, QRegularExpression -from PyQt6.QtGui import QRegularExpressionValidator -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt, QRegularExpression +from PySide6.QtGui import QRegularExpressionValidator +from PySide6.QtWidgets import ( QDialog, QFormLayout, QVBoxLayout, diff --git a/tilia/ui/dialogs/file.py b/tilia/ui/dialogs/file.py index 649f75f04..3e59fa975 100644 --- a/tilia/ui/dialogs/file.py +++ b/tilia/ui/dialogs/file.py @@ -1,9 +1,9 @@ from pathlib import Path from typing import Sequence -from PyQt6 import QtCore -from PyQt6.QtCore import QDir -from PyQt6.QtWidgets import QFileDialog +from PySide6 import QtCore +from PySide6.QtCore import QDir +from PySide6.QtWidgets import QFileDialog from tilia.media import constants as media_constants from tilia.constants import APP_NAME, FILE_EXTENSION diff --git a/tilia/ui/dialogs/harmony_params.py b/tilia/ui/dialogs/harmony_params.py index 4edc69d01..c0e38577b 100644 --- a/tilia/ui/dialogs/harmony_params.py +++ b/tilia/ui/dialogs/harmony_params.py @@ -1,5 +1,5 @@ import music21 -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDialog, QComboBox, QGridLayout, diff --git a/tilia/ui/dialogs/mode_params.py b/tilia/ui/dialogs/mode_params.py index 4d64671d4..3b895b703 100644 --- a/tilia/ui/dialogs/mode_params.py +++ b/tilia/ui/dialogs/mode_params.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDialog, QComboBox, QGridLayout, diff --git a/tilia/ui/dialogs/resize_rect.py b/tilia/ui/dialogs/resize_rect.py index 45a2d2b8c..7f5c74559 100644 --- a/tilia/ui/dialogs/resize_rect.py +++ b/tilia/ui/dialogs/resize_rect.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QDoubleSpinBox, diff --git a/tilia/ui/menubar.py b/tilia/ui/menubar.py index 478456359..e995c1694 100644 --- a/tilia/ui/menubar.py +++ b/tilia/ui/menubar.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import QMainWindow +from PySide6.QtWidgets import QMainWindow from tilia.ui.menus import FileMenu, EditMenu, TimelinesMenu, ViewMenu, HelpMenu diff --git a/tilia/ui/menus.py b/tilia/ui/menus.py index 0a1ede5f4..df34ce205 100644 --- a/tilia/ui/menus.py +++ b/tilia/ui/menus.py @@ -3,8 +3,8 @@ from typing import TypeAlias from enum import Enum, auto -from PyQt6.QtWidgets import QMenu -from PyQt6.QtGui import QAction +from PySide6.QtWidgets import QMenu +from PySide6.QtGui import QAction from tilia.timelines.timeline_kinds import get_timeline_name, TimelineKind from tilia.ui import commands diff --git a/tilia/ui/options_toolbar.py b/tilia/ui/options_toolbar.py index 41a6716a1..f1542c2c4 100644 --- a/tilia/ui/options_toolbar.py +++ b/tilia/ui/options_toolbar.py @@ -1,5 +1,5 @@ -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QToolBar, QComboBox, QLabel +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QToolBar, QComboBox, QLabel from tilia.settings import settings from tilia.ui.enums import ScrollType diff --git a/tilia/ui/player.py b/tilia/ui/player.py index f8faa2e77..8cb65ac2a 100644 --- a/tilia/ui/player.py +++ b/tilia/ui/player.py @@ -1,11 +1,11 @@ -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDoubleSpinBox, QLabel, QSlider, QToolBar, ) -from PyQt6.QtGui import QIcon, QAction, QPixmap -from PyQt6.QtCore import Qt +from PySide6.QtGui import QIcon, QAction, QPixmap +from PySide6.QtCore import Qt from tilia.dirs import IMG_DIR from tilia.ui import commands diff --git a/tilia/ui/qtui.py b/tilia/ui/qtui.py index a39b0047e..e8248cab4 100644 --- a/tilia/ui/qtui.py +++ b/tilia/ui/qtui.py @@ -6,10 +6,10 @@ from typing import Optional -from PyQt6 import QtGui -from PyQt6.QtCore import QKeyCombination, Qt, qInstallMessageHandler, QUrl, QtMsgType -from PyQt6.QtGui import QIcon, QFontDatabase, QDesktopServices, QPainter, QPixmap -from PyQt6.QtWidgets import ( +from PySide6 import QtGui +from PySide6.QtCore import QKeyCombination, Qt, qInstallMessageHandler, QUrl, QtMsgType +from PySide6.QtGui import QIcon, QFontDatabase, QDesktopServices, QPainter, QPixmap +from PySide6.QtWidgets import ( QMainWindow, QApplication, QToolBar, diff --git a/tilia/ui/smooth_scroll.py b/tilia/ui/smooth_scroll.py index 78f7756dd..64b853378 100644 --- a/tilia/ui/smooth_scroll.py +++ b/tilia/ui/smooth_scroll.py @@ -3,7 +3,7 @@ # - apply smoothing curve to input - currently linear from typing import Any, Callable -from PyQt6.QtCore import QVariantAnimation, QVariant +from PySide6.QtCore import QVariantAnimation from tilia.settings import settings @@ -13,7 +13,7 @@ def setup_smooth(self): self.animation.setDuration(125) -def smooth(self: Any, args_getter: Callable[[], QVariant]): +def smooth(self: Any, args_getter: Callable[[], object]): """ Function Wrapper Smooths changes made by `args_setter` by inputting smaller changes over time. @@ -26,15 +26,15 @@ def smooth(self: Any, args_getter: Callable[[], QVariant]): `args_getter` and `args_setter` must refer to the same variables in `args_setpoint` in the same order. """ - def wrapper(args_setter: Callable[[QVariant], None]) -> Callable: - def wrapped_setter(args_setpoint: QVariant) -> None: + def wrapper(args_setter: Callable[[object], None]) -> Callable: + def wrapped_setter(args_setpoint: object) -> None: if self.animation.state() is QVariantAnimation.State.Running: self.animation.pause() self.animation.setStartValue(args_getter()) self.animation.setEndValue(args_setpoint) self.animation.start() - def timeout(value: QVariant) -> None: + def timeout(value: object) -> None: args_setter(value) if settings.get("general", "prioritise_performance") is True: diff --git a/tilia/ui/timelines/audiowave/element.py b/tilia/ui/timelines/audiowave/element.py index 949158e6a..1404bea95 100644 --- a/tilia/ui/timelines/audiowave/element.py +++ b/tilia/ui/timelines/audiowave/element.py @@ -1,8 +1,8 @@ from __future__ import annotations -from PyQt6.QtCore import Qt, QPointF, QLineF -from PyQt6.QtGui import QPen, QColor -from PyQt6.QtWidgets import QGraphicsLineItem +from PySide6.QtCore import Qt, QPointF, QLineF +from PySide6.QtGui import QPen, QColor +from PySide6.QtWidgets import QGraphicsLineItem from tilia.requests import Post, post, get, Get from ..cursors import CursorMixIn diff --git a/tilia/ui/timelines/base/element.py b/tilia/ui/timelines/base/element.py index 59c54181c..622ec4fcc 100644 --- a/tilia/ui/timelines/base/element.py +++ b/tilia/ui/timelines/base/element.py @@ -4,12 +4,12 @@ from typing import Optional, Any, Callable -from PyQt6.QtCore import QPoint +from PySide6.QtCore import QPoint from tilia.ui.timelines.base.context_menus import TimelineUIElementContextMenu from tilia.ui.coords import time_x_converter -from PyQt6.QtWidgets import QGraphicsScene +from PySide6.QtWidgets import QGraphicsScene from tilia.requests import stop_listening_to_all diff --git a/tilia/ui/timelines/base/element_manager.py b/tilia/ui/timelines/base/element_manager.py index 302aaf528..cb4360c8f 100644 --- a/tilia/ui/timelines/base/element_manager.py +++ b/tilia/ui/timelines/base/element_manager.py @@ -3,7 +3,7 @@ import bisect from typing import Any, Callable, TYPE_CHECKING, TypeVar, Generic, Iterable -from PyQt6.QtWidgets import QGraphicsItem +from PySide6.QtWidgets import QGraphicsItem from tilia.timelines.component_kinds import ComponentKind from tilia.ui.timelines.element_kinds import get_element_class_by_kind diff --git a/tilia/ui/timelines/base/timeline.py b/tilia/ui/timelines/base/timeline.py index 0606fd2d5..104a8788c 100644 --- a/tilia/ui/timelines/base/timeline.py +++ b/tilia/ui/timelines/base/timeline.py @@ -12,8 +12,8 @@ Callable, ) -from PyQt6.QtCore import Qt, QPoint -from PyQt6.QtWidgets import QGraphicsItem +from PySide6.QtCore import Qt, QPoint +from PySide6.QtWidgets import QGraphicsItem from tilia.timelines.component_kinds import ComponentKind from .context_menus import TimelineUIContextMenu diff --git a/tilia/ui/timelines/beat/element.py b/tilia/ui/timelines/beat/element.py index 9a5e6b5ab..41a430862 100644 --- a/tilia/ui/timelines/beat/element.py +++ b/tilia/ui/timelines/beat/element.py @@ -1,9 +1,9 @@ from __future__ import annotations from typing import TYPE_CHECKING, Callable, Any -from PyQt6.QtCore import Qt, QLineF, QPointF -from PyQt6.QtGui import QPen, QColor, QFont -from PyQt6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsTextItem +from PySide6.QtCore import Qt, QLineF, QPointF +from PySide6.QtGui import QPen, QColor, QFont +from PySide6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsTextItem from tilia.requests import Post, post, Get, get from .context_menu import BeatContextMenu @@ -42,7 +42,8 @@ class BeatUI(TimelineUIElement): FIELD_NAMES_TO_ATTRIBUTES: dict[ str, str - ] = {} # only needed if attrs will be set by Inspect + ] = {} + # only needed if attrs will be set by Inspect DEFAULT_COPY_ATTRIBUTES = CopyAttributes( by_element_value=[], diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index ded463510..252760796 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -5,8 +5,8 @@ from enum import Enum, auto from typing import Any, Optional, Callable, cast -from PyQt6.QtCore import Qt, QPoint -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt, QPoint +from PySide6.QtWidgets import ( QGraphicsView, QMainWindow, QGraphicsItem, diff --git a/tilia/ui/timelines/collection/scene.py b/tilia/ui/timelines/collection/scene.py index 7d14d73fb..921cd8f8a 100644 --- a/tilia/ui/timelines/collection/scene.py +++ b/tilia/ui/timelines/collection/scene.py @@ -1,6 +1,6 @@ from __future__ import annotations -from PyQt6.QtWidgets import QGraphicsScene +from PySide6.QtWidgets import QGraphicsScene class TimelineUIsScene(QGraphicsScene): diff --git a/tilia/ui/timelines/collection/view.py b/tilia/ui/timelines/collection/view.py index d0d4d9de4..6e7387649 100644 --- a/tilia/ui/timelines/collection/view.py +++ b/tilia/ui/timelines/collection/view.py @@ -1,7 +1,7 @@ from __future__ import annotations -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QGraphicsView, QAbstractSlider +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QGraphicsView, QAbstractSlider from tilia.ui import commands from tilia.ui.smooth_scroll import setup_smooth, smooth diff --git a/tilia/ui/timelines/cursors.py b/tilia/ui/timelines/cursors.py index 95949187a..f66d417e1 100644 --- a/tilia/ui/timelines/cursors.py +++ b/tilia/ui/timelines/cursors.py @@ -1,4 +1,4 @@ -from PyQt6.QtGui import QGuiApplication +from PySide6.QtGui import QGuiApplication # noinspection PyUnresolvedReferences diff --git a/tilia/ui/timelines/harmony/elements/harmony.py b/tilia/ui/timelines/harmony/elements/harmony.py index ec5bf195d..f496c521d 100644 --- a/tilia/ui/timelines/harmony/elements/harmony.py +++ b/tilia/ui/timelines/harmony/elements/harmony.py @@ -1,9 +1,9 @@ from __future__ import annotations import music21 -from PyQt6.QtCore import QPointF -from PyQt6.QtGui import QFont, QColor -from PyQt6.QtWidgets import QGraphicsItem, QGraphicsTextItem +from PySide6.QtCore import QPointF +from PySide6.QtGui import QFont, QColor +from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem from music21.roman import RomanNumeral from . import harmony_attrs diff --git a/tilia/ui/timelines/harmony/elements/mode.py b/tilia/ui/timelines/harmony/elements/mode.py index 05df9a375..89076aa4a 100644 --- a/tilia/ui/timelines/harmony/elements/mode.py +++ b/tilia/ui/timelines/harmony/elements/mode.py @@ -1,7 +1,7 @@ import music21 -from PyQt6.QtCore import QPointF -from PyQt6.QtGui import QFont, QColor -from PyQt6.QtWidgets import QGraphicsItem, QGraphicsTextItem +from PySide6.QtCore import QPointF +from PySide6.QtGui import QFont, QColor +from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem from tilia.requests import get, Get, post, Post from tilia.ui.coords import time_x_converter diff --git a/tilia/ui/timelines/harmony/level_label.py b/tilia/ui/timelines/harmony/level_label.py index 0f87c6ba1..4506a8874 100644 --- a/tilia/ui/timelines/harmony/level_label.py +++ b/tilia/ui/timelines/harmony/level_label.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import QPointF -from PyQt6.QtGui import QFont, QColor -from PyQt6.QtWidgets import QGraphicsTextItem +from PySide6.QtCore import QPointF +from PySide6.QtGui import QFont, QColor +from PySide6.QtWidgets import QGraphicsTextItem class LevelLabel(QGraphicsTextItem): diff --git a/tilia/ui/timelines/hierarchy/element.py b/tilia/ui/timelines/hierarchy/element.py index c7f1f62e7..1948b98c6 100644 --- a/tilia/ui/timelines/hierarchy/element.py +++ b/tilia/ui/timelines/hierarchy/element.py @@ -3,9 +3,9 @@ from enum import Enum from typing import Literal -from PyQt6.QtCore import Qt, QRectF, QPointF -from PyQt6.QtGui import QColor, QPen, QFont, QFontMetrics, QPixmap -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt, QRectF, QPointF +from PySide6.QtGui import QColor, QPen, QFont, QFontMetrics, QPixmap +from PySide6.QtWidgets import ( QGraphicsPixmapItem, QGraphicsRectItem, QGraphicsTextItem, diff --git a/tilia/ui/timelines/hierarchy/handles.py b/tilia/ui/timelines/hierarchy/handles.py index 033fe3c6f..36495f97a 100644 --- a/tilia/ui/timelines/hierarchy/handles.py +++ b/tilia/ui/timelines/hierarchy/handles.py @@ -1,8 +1,8 @@ from __future__ import annotations -from PyQt6.QtCore import Qt, QRectF, QPointF, QLineF -from PyQt6.QtGui import QColor, QPen -from PyQt6.QtWidgets import QGraphicsRectItem, QGraphicsLineItem, QGraphicsItemGroup +from PySide6.QtCore import Qt, QRectF, QPointF, QLineF +from PySide6.QtGui import QColor, QPen +from PySide6.QtWidgets import QGraphicsRectItem, QGraphicsLineItem, QGraphicsItemGroup from tilia.ui.timelines.cursors import CursorMixIn diff --git a/tilia/ui/timelines/marker/element.py b/tilia/ui/timelines/marker/element.py index 6871c7856..578e7be1e 100644 --- a/tilia/ui/timelines/marker/element.py +++ b/tilia/ui/timelines/marker/element.py @@ -1,8 +1,8 @@ from __future__ import annotations -from PyQt6.QtCore import QPointF, Qt -from PyQt6.QtGui import QPolygonF, QPen, QColor, QFont -from PyQt6.QtWidgets import ( +from PySide6.QtCore import QPointF, Qt +from PySide6.QtGui import QPolygonF, QPen, QColor, QFont +from PySide6.QtWidgets import ( QGraphicsItem, QGraphicsPolygonItem, QGraphicsTextItem, diff --git a/tilia/ui/timelines/pdf/element.py b/tilia/ui/timelines/pdf/element.py index 0a1c8c739..759d5e503 100644 --- a/tilia/ui/timelines/pdf/element.py +++ b/tilia/ui/timelines/pdf/element.py @@ -1,14 +1,14 @@ from __future__ import annotations -from PyQt6.QtCore import QPointF, Qt -from PyQt6.QtGui import ( +from PySide6.QtCore import QPointF, Qt +from PySide6.QtGui import ( QPen, QColor, QFont, QPixmap, QFontMetrics, ) -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QGraphicsItem, QGraphicsTextItem, QGraphicsPixmapItem, diff --git a/tilia/ui/timelines/pdf/timeline.py b/tilia/ui/timelines/pdf/timeline.py index 896f24e9f..5f83a47b8 100644 --- a/tilia/ui/timelines/pdf/timeline.py +++ b/tilia/ui/timelines/pdf/timeline.py @@ -3,9 +3,9 @@ import copy from typing import TYPE_CHECKING -from PyQt6.QtCore import QPointF -from PyQt6.QtPdf import QPdfDocument -from PyQt6.QtPdfWidgets import QPdfView +from PySide6.QtCore import QPointF +from PySide6.QtPdf import QPdfDocument +from PySide6.QtPdfWidgets import QPdfView import tilia.errors from tilia.media.player.base import MediaTimeChangeReason diff --git a/tilia/ui/timelines/scene.py b/tilia/ui/timelines/scene.py index b80c1bca5..7601dbeab 100644 --- a/tilia/ui/timelines/scene.py +++ b/tilia/ui/timelines/scene.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QGraphicsScene, QGraphicsRectItem -from PyQt6.QtGui import QColor, QPen, QBrush, QFont, QFontMetrics +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QGraphicsScene, QGraphicsRectItem +from PySide6.QtGui import QColor, QPen, QBrush, QFont, QFontMetrics from tilia.settings import settings from tilia.requests import Get, get, listen, Post diff --git a/tilia/ui/timelines/score/element/barline.py b/tilia/ui/timelines/score/element/barline.py index a3bdcb2cd..f62cb3d57 100644 --- a/tilia/ui/timelines/score/element/barline.py +++ b/tilia/ui/timelines/score/element/barline.py @@ -1,5 +1,5 @@ -from PyQt6.QtGui import QPen, QColor -from PyQt6.QtWidgets import QGraphicsLineItem +from PySide6.QtGui import QPen, QColor +from PySide6.QtWidgets import QGraphicsLineItem from tilia.ui.coords import time_x_converter from tilia.ui.timelines.base.element import TimelineUIElement diff --git a/tilia/ui/timelines/score/element/clef.py b/tilia/ui/timelines/score/element/clef.py index 377350d44..cf5814ee3 100644 --- a/tilia/ui/timelines/score/element/clef.py +++ b/tilia/ui/timelines/score/element/clef.py @@ -1,8 +1,8 @@ from pathlib import Path -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import QGraphicsPixmapItem +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QGraphicsPixmapItem from tilia.dirs import IMG_DIR from tilia.timelines.score.components import Clef diff --git a/tilia/ui/timelines/score/element/key_signature.py b/tilia/ui/timelines/score/element/key_signature.py index 2d7577aef..baa64b080 100644 --- a/tilia/ui/timelines/score/element/key_signature.py +++ b/tilia/ui/timelines/score/element/key_signature.py @@ -1,8 +1,8 @@ from __future__ import annotations -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import QGraphicsPixmapItem +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QGraphicsPixmapItem from tilia.dirs import IMG_DIR from tilia.timelines.score.components import Clef diff --git a/tilia/ui/timelines/score/element/note/accidental.py b/tilia/ui/timelines/score/element/note/accidental.py index e6b9c4fb5..0f79297a4 100644 --- a/tilia/ui/timelines/score/element/note/accidental.py +++ b/tilia/ui/timelines/score/element/note/accidental.py @@ -1,8 +1,8 @@ from pathlib import Path -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import QGraphicsPixmapItem +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QGraphicsPixmapItem class NoteAccidental(QGraphicsPixmapItem): diff --git a/tilia/ui/timelines/score/element/note/body.py b/tilia/ui/timelines/score/element/note/body.py index 056944f29..03c7da390 100644 --- a/tilia/ui/timelines/score/element/note/body.py +++ b/tilia/ui/timelines/score/element/note/body.py @@ -1,8 +1,8 @@ from __future__ import annotations -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QColor, QPen -from PyQt6.QtWidgets import QGraphicsRectItem +from PySide6.QtCore import Qt +from PySide6.QtGui import QColor, QPen +from PySide6.QtWidgets import QGraphicsRectItem from tilia.timelines.score.components import Note from tilia.ui.color import get_tinted_color, get_untinted_color diff --git a/tilia/ui/timelines/score/element/note/ledger_line.py b/tilia/ui/timelines/score/element/note/ledger_line.py index cb093d7ad..4b60a594f 100644 --- a/tilia/ui/timelines/score/element/note/ledger_line.py +++ b/tilia/ui/timelines/score/element/note/ledger_line.py @@ -1,8 +1,8 @@ from enum import Enum -from PyQt6.QtCore import QLineF -from PyQt6.QtGui import QPen, QColor -from PyQt6.QtWidgets import QGraphicsLineItem +from PySide6.QtCore import QLineF +from PySide6.QtGui import QPen, QColor +from PySide6.QtWidgets import QGraphicsLineItem class NoteLedgerLines: diff --git a/tilia/ui/timelines/score/element/note/ui.py b/tilia/ui/timelines/score/element/note/ui.py index 3b9f624d6..f09e31d09 100644 --- a/tilia/ui/timelines/score/element/note/ui.py +++ b/tilia/ui/timelines/score/element/note/ui.py @@ -1,7 +1,7 @@ import math from pathlib import Path -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QGraphicsItem, ) diff --git a/tilia/ui/timelines/score/element/staff.py b/tilia/ui/timelines/score/element/staff.py index 856500456..87007a054 100644 --- a/tilia/ui/timelines/score/element/staff.py +++ b/tilia/ui/timelines/score/element/staff.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import QLineF -from PyQt6.QtGui import QColor, QPen -from PyQt6.QtWidgets import QGraphicsLineItem +from PySide6.QtCore import QLineF +from PySide6.QtGui import QColor, QPen +from PySide6.QtWidgets import QGraphicsLineItem from tilia.requests import get, Get from tilia.timelines.component_kinds import ComponentKind diff --git a/tilia/ui/timelines/score/element/time_signature.py b/tilia/ui/timelines/score/element/time_signature.py index 7a5c187fc..f31f7cfcc 100644 --- a/tilia/ui/timelines/score/element/time_signature.py +++ b/tilia/ui/timelines/score/element/time_signature.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import QGraphicsItem, QGraphicsPixmapItem +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QGraphicsItem, QGraphicsPixmapItem from tilia.ui.coords import time_x_converter from tilia.ui.timelines.score.element.with_collision import ( diff --git a/tilia/ui/timelines/score/timeline.py b/tilia/ui/timelines/score/timeline.py index bf0151e5a..b7739f1b9 100644 --- a/tilia/ui/timelines/score/timeline.py +++ b/tilia/ui/timelines/score/timeline.py @@ -3,9 +3,9 @@ import math from typing import Callable, Any, Iterable -from PyQt6.QtCore import Qt, QRectF, QPointF -from PyQt6.QtGui import QPixmap, QColor -from PyQt6.QtWidgets import QGraphicsRectItem +from PySide6.QtCore import Qt, QRectF, QPointF +from PySide6.QtGui import QPixmap, QColor +from PySide6.QtWidgets import QGraphicsRectItem from tilia.dirs import IMG_DIR import tilia.errors diff --git a/tilia/ui/timelines/selection_box.py b/tilia/ui/timelines/selection_box.py index 296a2d960..8549fbd17 100644 --- a/tilia/ui/timelines/selection_box.py +++ b/tilia/ui/timelines/selection_box.py @@ -1,6 +1,6 @@ -from PyQt6.QtCore import QRectF, QPointF, Qt -from PyQt6.QtGui import QColor, QPen -from PyQt6.QtWidgets import QGraphicsRectItem +from PySide6.QtCore import QRectF, QPointF, Qt +from PySide6.QtGui import QColor, QPen +from PySide6.QtWidgets import QGraphicsRectItem from tilia.requests import Post, stop_listening_to_all, post diff --git a/tilia/ui/timelines/slider/timeline.py b/tilia/ui/timelines/slider/timeline.py index 19ef75802..65afbebcc 100644 --- a/tilia/ui/timelines/slider/timeline.py +++ b/tilia/ui/timelines/slider/timeline.py @@ -1,9 +1,9 @@ from __future__ import annotations from typing import TYPE_CHECKING -from PyQt6.QtCore import QRectF, QLineF, Qt -from PyQt6.QtGui import QPen, QColor, QBrush -from PyQt6.QtWidgets import ( +from PySide6.QtCore import QRectF, QLineF, Qt +from PySide6.QtGui import QPen, QColor, QBrush +from PySide6.QtWidgets import ( QGraphicsEllipseItem, QGraphicsLineItem, QGraphicsDropShadowEffect, diff --git a/tilia/ui/timelines/toolbar.py b/tilia/ui/timelines/toolbar.py index 00232bb87..e92efd09a 100644 --- a/tilia/ui/timelines/toolbar.py +++ b/tilia/ui/timelines/toolbar.py @@ -1,5 +1,5 @@ -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QToolBar +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QToolBar from tilia.ui import commands diff --git a/tilia/ui/timelines/view.py b/tilia/ui/timelines/view.py index 9ad1581f8..5fe292fc9 100644 --- a/tilia/ui/timelines/view.py +++ b/tilia/ui/timelines/view.py @@ -1,14 +1,14 @@ from typing import Optional -from PyQt6.QtCore import Qt -from PyQt6.QtGui import ( +from PySide6.QtCore import Qt +from PySide6.QtGui import ( QPainter, QMouseEvent, QGuiApplication, QColor, QBrush, ) -from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QSizePolicy, QFrame +from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QSizePolicy, QFrame from tilia.settings import settings from tilia.requests import post, Post, Get, get, listen diff --git a/tilia/ui/windows/about.py b/tilia/ui/windows/about.py index f715874f6..a2b3a3106 100644 --- a/tilia/ui/windows/about.py +++ b/tilia/ui/windows/about.py @@ -1,6 +1,6 @@ from pathlib import Path -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QDialog, QFrame, QLabel, diff --git a/tilia/ui/windows/beat_pattern.py b/tilia/ui/windows/beat_pattern.py index 1ed9566f8..edf495b8c 100644 --- a/tilia/ui/windows/beat_pattern.py +++ b/tilia/ui/windows/beat_pattern.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import QInputDialog +from PySide6.QtWidgets import QInputDialog import tilia.ui.strings diff --git a/tilia/ui/windows/fill_beat_timeline.py b/tilia/ui/windows/fill_beat_timeline.py index 330f63a7b..65511eae9 100644 --- a/tilia/ui/windows/fill_beat_timeline.py +++ b/tilia/ui/windows/fill_beat_timeline.py @@ -1,6 +1,6 @@ import sys -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QButtonGroup, QGridLayout, QRadioButton, diff --git a/tilia/ui/windows/inspect.py b/tilia/ui/windows/inspect.py index 5c1afd3ad..4cec20757 100644 --- a/tilia/ui/windows/inspect.py +++ b/tilia/ui/windows/inspect.py @@ -5,7 +5,7 @@ from typing import Any, Callable, cast -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDockWidget, QFormLayout, QLabel, @@ -19,7 +19,7 @@ QFrame, ) -from PyQt6.QtCore import Qt, QKeyCombination +from PySide6.QtCore import Qt, QKeyCombination from tilia.requests import Post, listen, stop_listening_to_all, post, Get, get from tilia.utils import get_tilia_class_string diff --git a/tilia/ui/windows/manage_timelines.py b/tilia/ui/windows/manage_timelines.py index 321befe52..3840e3608 100644 --- a/tilia/ui/windows/manage_timelines.py +++ b/tilia/ui/windows/manage_timelines.py @@ -1,9 +1,9 @@ from typing import Optional import typing -from PyQt6 import QtGui -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import ( +from PySide6 import QtGui +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, diff --git a/tilia/ui/windows/metadata.py b/tilia/ui/windows/metadata.py index d721c7b44..7eff335e3 100644 --- a/tilia/ui/windows/metadata.py +++ b/tilia/ui/windows/metadata.py @@ -1,6 +1,6 @@ import functools -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QDialog, QFormLayout, QLabel, diff --git a/tilia/ui/windows/metadata_edit_fields.py b/tilia/ui/windows/metadata_edit_fields.py index a4df3a08a..9159c1b03 100644 --- a/tilia/ui/windows/metadata_edit_fields.py +++ b/tilia/ui/windows/metadata_edit_fields.py @@ -1,6 +1,6 @@ import functools -from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox +from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox from tilia.requests import Get, get diff --git a/tilia/ui/windows/metadata_edit_notes.py b/tilia/ui/windows/metadata_edit_notes.py index b10d590a0..d67764bb0 100644 --- a/tilia/ui/windows/metadata_edit_notes.py +++ b/tilia/ui/windows/metadata_edit_notes.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox +from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox from tilia.requests import Get, get diff --git a/tilia/ui/windows/settings.py b/tilia/ui/windows/settings.py index 368729070..b6e648e3c 100644 --- a/tilia/ui/windows/settings.py +++ b/tilia/ui/windows/settings.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import ( +from PySide6.QtWidgets import ( QCheckBox, QColorDialog, QComboBox, @@ -15,7 +15,7 @@ QVBoxLayout, QWidget, ) -from PyQt6.QtGui import QColor +from PySide6.QtGui import QColor from tilia.settings import settings from tilia.requests import post, Post diff --git a/tilia/ui/windows/svg_viewer.py b/tilia/ui/windows/svg_viewer.py index f08f01f16..db247938f 100644 --- a/tilia/ui/windows/svg_viewer.py +++ b/tilia/ui/windows/svg_viewer.py @@ -6,14 +6,14 @@ from bisect import bisect from typing import Callable -from PyQt6.QtCore import ( +from PySide6.QtCore import ( Qt, QKeyCombination, QPointF, ) -from PyQt6.QtGui import QFont -from PyQt6.QtSvgWidgets import QGraphicsSvgItem -from PyQt6.QtWidgets import ( +from PySide6.QtGui import QFont +from PySide6.QtSvgWidgets import QGraphicsSvgItem +from PySide6.QtWidgets import ( QFrame, QGraphicsView, QGraphicsItem, @@ -24,7 +24,7 @@ QVBoxLayout, QWidget, ) -from PyQt6.QtSvg import QSvgRenderer +from PySide6.QtSvg import QSvgRenderer from tilia.ui import commands from tilia.ui.commands import get_qaction diff --git a/tilia/ui/windows/view_window.py b/tilia/ui/windows/view_window.py index 1b7da4458..68268a349 100644 --- a/tilia/ui/windows/view_window.py +++ b/tilia/ui/windows/view_window.py @@ -1,7 +1,7 @@ from typing import TypeVar, Generic -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QDockWidget, QDialog, QWidget +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDockWidget, QDialog, QWidget from tilia.requests import get, Get, post, Post, listen from tilia.ui.enums import WindowState From 3fae9f42794a928323ed7b9aa4ee51a0afc1d090 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:07:14 +0100 Subject: [PATCH 06/75] fix: returns before event loops complete A tiny patch on one of the bigger architectural issues - many of Qt's modules run on event loops and we don't tend to run our end as an event driven system. This fixes some of it by waiting for Qt to respond with the corresponding signal before returning control to the user. --- tilia/media/player/qtplayer.py | 57 +++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py index bb4659c1b..611ff8b15 100644 --- a/tilia/media/player/qtplayer.py +++ b/tilia/media/player/qtplayer.py @@ -1,8 +1,6 @@ from __future__ import annotations -import time - -from PySide6.QtCore import QUrl +from PySide6.QtCore import QUrl, QEventLoop, SignalInstance, QTimer from PySide6.QtMultimedia import ( QMediaPlayer, QAudioOutput, @@ -17,6 +15,33 @@ from tilia.ui.player import PlayerStatus +def wait_for_signal(signal: SignalInstance, value): + def signal_wrapper(func): + timer = QTimer(singleShot=True, interval=100) + loop = QEventLoop() + success = False + + def value_checker(signal_value): + if signal_value == value: + global success + success = True + loop.quit() + + def check_signal(*args, **kwargs): + global success # noqa: F824 + if not func(*args, **kwargs): + return False + signal.connect(value_checker) + timer.timeout.connect(loop.quit) + timer.start() + loop.exec() + return timer.isActive() and success + + return check_signal + + return signal_wrapper + + class QtPlayer(Player): MEDIA_TYPE = "" @@ -48,9 +73,15 @@ def on_audio_outputs_changed(self) -> None: self.audio_output.setDevice(QAudioDevice()) def _engine_load_media(self, media_path: str) -> bool: - self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze. - self.player.setSource(QUrl.fromLocalFile(media_path)) - return True + @wait_for_signal( + self.player.mediaStatusChanged, QMediaPlayer.MediaStatus.LoadedMedia + ) + def load_media(media_path): + self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze. + self.player.setSource(QUrl.fromLocalFile(media_path)) + return True + + return load_media(media_path) def _engine_get_current_time(self): return self.player.position() / 1000 @@ -68,12 +99,14 @@ def _engine_unpause(self) -> None: self.player.play() def _engine_stop(self): - self.player.stop() - # Sleeping avoids freeze if about to change player URL. Not sure why the freeze happens. - # Waiting for self.player.mediaStatus() == MediaPlayer.MediaStatus.Stopped also does not work. - # I have tested different sleep times and 0.01 seems to prevent freezes reliably - # while still having no perceptible impact on test performance. - time.sleep(0.01) + @wait_for_signal( + self.player.playbackStateChanged, QMediaPlayer.PlaybackState.StoppedState + ) + def stop(): + self.player.stop() + return True + + return stop() def _engine_unload_media(self): self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze. From 053249ea6bfab9b7c83b5dcf504d04453246a5de Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:45:27 +0100 Subject: [PATCH 07/75] fix: incorrect kwarg name --- tilia/ui/timelines/beat/timeline.py | 4 ++-- tilia/ui/timelines/collection/collection.py | 4 ++-- tilia/ui/timelines/collection/requests/args.py | 0 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 tilia/ui/timelines/collection/requests/args.py diff --git a/tilia/ui/timelines/beat/timeline.py b/tilia/ui/timelines/beat/timeline.py index cba78b600..78597faab 100644 --- a/tilia/ui/timelines/beat/timeline.py +++ b/tilia/ui/timelines/beat/timeline.py @@ -116,7 +116,7 @@ def on_set_measure_number(self, elements: list[BeatUI] | None = None): Get.FROM_USER_INT, "Change measure number", "Insert measure number", - min=0, + minValue=0, ) if not accepted: return False @@ -143,7 +143,7 @@ def on_set_amount_in_measure(self, elements: list[BeatUI] | None = None): Get.FROM_USER_INT, "Change beats in measure", "Insert amount of beats in measure", - min=1, + minValue=1, ) if not accepted: return False diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index 252760796..5c5ea46f0 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -386,7 +386,7 @@ def _handle_media_not_loaded() -> bool: "Set duration", "Insert duration (s)", value=60, - min=1, + minValue=1, ) if not success: return False @@ -1195,7 +1195,7 @@ def on_timeline_set_height( "Change timeline height", "Insert new timeline height", value=timeline_ui.get_data("height"), - min=10, + minValue=10, ) if not accepted: return False diff --git a/tilia/ui/timelines/collection/requests/args.py b/tilia/ui/timelines/collection/requests/args.py new file mode 100644 index 000000000..e69de29bb From 9c5883fa6a88569f1441994bce3b890c23beaded Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:53:22 +0100 Subject: [PATCH 08/75] ci: test does not require screen definition already set to offscreen in env. --- .github/workflows/run-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ada19a0c2..9b926fec6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -35,7 +35,6 @@ jobs: path: ~\AppData\Local\pip\Cache timeout-minutes: 30 env: - DISPLAY: ":99.0" QT_SELECT: "qt6" steps: @@ -51,7 +50,6 @@ jobs: sudo apt-get update sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 libxcb-shape0 libglib2.0-0 libgl1-mesa-dev libpulse-dev sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev - sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 & - uses: actions/cache@v4 with: From 0ad3c3e945e5ad87c1237e40aa2fcb06eb7c2368 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:54:45 +0100 Subject: [PATCH 09/75] ci: min ver PySide 6.8 set for long-term support (see Qt's support page) --- requirements.txt | 6 +++--- setup.cfg | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index ba82e100c..ca7273e58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ prettytable>=3.7.0 lxml>=5.3.0 music21>=9.1 numpy~=1.26.4 -PySide6>=6.9.0 -PySide6_Addons>=6.9.0 -PySide6_Essentials>=6.9.0 +PySide6>=6.8.0 +PySide6_Addons>=6.8.0 +PySide6_Essentials>=6.8.0 python-dotenv>=1.0.1 pypdf>=6.0.0 httpx~=0.28.1 diff --git a/setup.cfg b/setup.cfg index 4e1911938..12582c701 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,9 +12,9 @@ install_requires = prettytable>=3.7.0 music21>=9.1 numpy~=1.26.4 - PySide6>=6.10.0 - PySide6_Addons>=6.10.0 - PySide6_Essentials>=6.10.0 + PySide6>=6.8.0 + PySide6_Addons>=6.8.0 + PySide6_Essentials>=6.8.0 python-dotenv>=1.0.1 pypdf~=4.2.0 sentry-sdk>=2.21.0 From dcb44af5b73d2a959caa55a5324bf65ad6f78d8b Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:55:34 +0100 Subject: [PATCH 10/75] ci: PySide currently supports Python [3.10 - 3.13] --- .github/workflows/run-tests.yml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9b926fec6..e7fe91735 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [macos-latest, ubuntu-latest, windows-latest] include: - os: ubuntu-latest diff --git a/setup.cfg b/setup.cfg index 12582c701..3861ef19b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ install_requires = httpx~=0.28.1 isodate~=0.7.2 colorama~=0.4.6 -python_requires = >=3.10 +python_requires=>=3.10,<3.14 zip_safe = no [options.extras_require] From 777c51bd6f52c51c9d9427cbb9e3452ce5f10a73 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:13:27 +0100 Subject: [PATCH 11/75] chore: remove outdated builder --- build.py | 59 -------------------------------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 build.py diff --git a/build.py b/build.py deleted file mode 100644 index d87a35cb2..000000000 --- a/build.py +++ /dev/null @@ -1,59 +0,0 @@ -import subprocess - -import tilia.constants - -from pathlib import Path - -PATH_TO_INNO = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" -PATH_TO_INSTALL_SCRIPT_TEMPLATE = Path("tilia", "installers", "win", "template.iss") -PATH_TO_INSTALL_SCRIPT = Path( - "tilia", "installers", "win", f"tilia_{tilia.constants.VERSION}.iss" -) - - -def confirm_version_number_update(): - answer = input( - f"Did you remember to update the version number (current version number is {tilia.constants.VERSION})? y/n " - ) - - if answer.lower() == "y": - return True - else: - return False - - -def build(): - p = subprocess.Popen("pyinstaller tilia.spec -y") - p.wait() - - -def make_installer(): - create_install_script() - - p = subprocess.Popen(f"{PATH_TO_INNO} {PATH_TO_INSTALL_SCRIPT.resolve()}") - p.wait() - - -def create_install_script() -> None: - with open(PATH_TO_INSTALL_SCRIPT_TEMPLATE, "r") as f: - template = f.read() - - install_script = template.replace("$VERSION$", tilia.constants.VERSION) - install_script = install_script.replace( - "$SOURCE_PATH$", Path("").resolve().__str__() - ) - - with open(PATH_TO_INSTALL_SCRIPT, "w") as f: - f.write(install_script) - - -def main() -> None: - if not confirm_version_number_update(): - return - - # build() - make_installer() - - -if __name__ == "__main__": - main() From f853da7d66aa8fa574b21cb90f4bf06e5c985264 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:23:35 +0100 Subject: [PATCH 12/75] build: centralise all dependencies in pyproject file --- pyproject.toml | 63 +++++++++++++++++++++++++++++++++++++++++++--- requirements.txt | 20 --------------- setup.cfg | 39 ---------------------------- setup.py | 4 --- tilia/constants.py | 27 ++++++++------------ 5 files changed, 71 insertions(+), 82 deletions(-) delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index 255236759..179be8f7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,67 @@ [build-system] -requires = ["setuptools>=78.1.1", "wheel"] +requires = [ + "setuptools>=78.1.1", + "wheel", +] build-backend = "setuptools.build_meta" -[tool.pytest.ini_options] -env = ['QT_QPA_PLATFORM=offscreen', 'ENVIRONMENT=test'] +[project] +name = "TiLiA" +version = "0.5.15" +description = "A GUI for creating and visualizing annotations over audio and video files." +license = "GPL-3.0" +license-files = ["LICENSE"] +readme = "README.md" +requires-python = ">=3.10,<3.14" +dependencies = [ + "colorama~=0.4.6", + "httpx~=0.28.1", + "isodate~=0.7.2", + "lxml>=5.3.0", + "music21>=9.1", + "numpy~=1.26.4", + "platformdirs>=4.0.0", + "prettytable>=3.7.0", + "python-dotenv>=1.0.1", + "pypdf>=6.0.0", + "PySide6>=6.8.0", + "PySide6_Addons>=6.8.0", + "PySide6_Essentials>=6.8.0", + "sentry-sdk>=2.21.0", + "soundfile~=0.13.1", + "TiLiA" +] + +[[project.authors]] +name = "Felipe Defensor et al." +email = "tilia@tilia-app.com" + +[project.gui-scripts] +tilia = "tilia.main:main" + +[project.urls] +Homepage = "https://tilia-app.com" +Repository = "https://github.com/TimeLineAnnotator/desktop" + +[project.optional-dependencies] +ci-tests = [ + "coverage>=7.0.0", + "coverage-badge>=1.0.0", + "flake8>=7.0.0", + "pytest-cov>=6.0.0", +] +dev = ["icecream>=2.1.0"] +testing = [ + "pytest>=8.0.0", + "pytest-env>=1.1.5", +] [tool.coverage.run] source = ["tilia"] omit = ["tilia/dev/*"] + +[tool.pytest.ini_options] +env = [ + "ENVIRONMENT=test", + "QT_QPA_PLATFORM=offscreen", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ca7273e58..000000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -platformdirs>=4.0.0 -prettytable>=3.7.0 -lxml>=5.3.0 -music21>=9.1 -numpy~=1.26.4 -PySide6>=6.8.0 -PySide6_Addons>=6.8.0 -PySide6_Essentials>=6.8.0 -python-dotenv>=1.0.1 -pypdf>=6.0.0 -httpx~=0.28.1 -isodate~=0.7.2 -soundfile~=0.13.1 -colorama~=0.4.6 - -pytest>=8.0.0 -pytest-env>=1.1.5 -icecream~=2.1.3 -colorama~=0.4.6 -sentry-sdk>=2.21.0 diff --git a/setup.cfg b/setup.cfg index 3861ef19b..1580f0938 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,42 +1,3 @@ -[metadata] -name = TiLiA -version = 0.5.15 -description = A GUI for creating and visualizing annotations over audio and video files. -author = Felipe Defensor et al. - -[options] -packages = tilia -install_requires = - lxml>=5.3.0 - platformdirs>=4.0.0 - prettytable>=3.7.0 - music21>=9.1 - numpy~=1.26.4 - PySide6>=6.8.0 - PySide6_Addons>=6.8.0 - PySide6_Essentials>=6.8.0 - python-dotenv>=1.0.1 - pypdf~=4.2.0 - sentry-sdk>=2.21.0 - soundfile~=0.13.1 - httpx~=0.28.1 - isodate~=0.7.2 - colorama~=0.4.6 -python_requires=>=3.10,<3.14 -zip_safe = no - -[options.extras_require] -testing = - pytest>=8.0.0 - pytest-env>=1.1.5 - colorama>=0.4.6 - icecream>=2.1.0 -ci-tests = - coverage>=7.0.0 - coverage-badge>=1.0.0 - flake8>=7.0.0 - pytest-cov>=6.0.0 - [flake8] max-line-length = 88 per-file-ignores = __init__.py: F401 diff --git a/setup.py b/setup.py deleted file mode 100644 index 7f1a1763c..000000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - -if __name__ == "__main__": - setup() diff --git a/tilia/constants.py b/tilia/constants.py index 37b00702e..57c420223 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -1,27 +1,22 @@ -import configparser +import tomllib from pathlib import Path -setupcfg = configparser.ConfigParser() -setupcfg.read(Path(__file__).parent.parent / "setup.cfg") +with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f: + setupcfg = tomllib.load(f).get("project", {}) -if setupcfg.has_section("metadata"): - APP_NAME = setupcfg["metadata"]["name"] - AUTHOR = setupcfg["metadata"]["author"] - VERSION = setupcfg["metadata"]["version"] - -else: - APP_NAME = "TiLiA" - AUTHOR = "" - VERSION = "beta" +APP_NAME = setupcfg.get("name", "") +AUTHOR = setupcfg.get("authors", [{"name": ""}])[0]["name"] +VERSION = setupcfg.get("version", "beta") YEAR = "2022-2025" FILE_EXTENSION = "tla" -EMAIL_URL = "mailto:tilia@tilia-app.com" -GITHUB_URL = "https://github.com/TimeLineAnnotator/desktop" -WEBSITE_URL = "https://tilia-app.com" +EMAIL_URL = "mailto:" + setupcfg.get("authors", [{"email": ""}])[0]["email"] + +GITHUB_URL = setupcfg.get("urls", {}).get("Repository", "") +WEBSITE_URL = setupcfg.get("urls", {}).get("Homepage", "") YOUTUBE_URL_REGEX = r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$" NOTICE = f""" -{APP_NAME}, {setupcfg["metadata"]["description"] if AUTHOR else ""} +{APP_NAME}, {setupcfg.get("description", "") if AUTHOR else ""} Copyright © {YEAR} {AUTHOR} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. From 8294d36c3e612bd477f673336265b997062c1744 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:35:28 +0100 Subject: [PATCH 13/75] fix: toml parser for python <3.11 --- tilia/constants.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tilia/constants.py b/tilia/constants.py index 57c420223..7fc47866e 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -1,8 +1,11 @@ -import tomllib +try: + from tomllib import load +except ModuleNotFoundError: + from tomli import load from pathlib import Path with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f: - setupcfg = tomllib.load(f).get("project", {}) + setupcfg = load(f).get("project", {}) APP_NAME = setupcfg.get("name", "") AUTHOR = setupcfg.get("authors", [{"name": ""}])[0]["name"] From 6f9b627f66a654c783d6793fe694292be77dd8f6 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:34:03 +0100 Subject: [PATCH 14/75] chore: remove unused files --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 29a3b0892..57b553bff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,7 @@ /.pytest_cache/ /build/ /dist/ -/tilia/installers/win/tilia* /tilia.egg-info/ /venv/ __pycache__/ -pytest.ini tilia/dev/ From 768661816445710ef3c6789726af0b6565aa2ebe Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:39:45 +0100 Subject: [PATCH 15/75] refactor: rename mostly to help nuitka find the entry point ...but with the added benefit of a little less typing day-to-day! --- README.md | 4 ++-- tilia/{main.py => __main__.py} | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) rename tilia/{main.py => __main__.py} (67%) diff --git a/README.md b/README.md index 0f268a3f1..a7fb226ae 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,12 @@ sudo apt install libnss3 libasound libxkbfile1 libpulse0 To run TiLiA from source, use: ``` -python -m tilia.main +python -m tilia ``` TiLiA also offers a CLI mode, which can be run with: ``` -python -m tilia.main --user-interface cli +python -m tilia --user-interface cli ``` ### Building from source diff --git a/tilia/main.py b/tilia/__main__.py similarity index 67% rename from tilia/main.py rename to tilia/__main__.py index 61175e8aa..9cc18b599 100644 --- a/tilia/main.py +++ b/tilia/__main__.py @@ -1,9 +1,5 @@ from tilia.boot import boot -def main() -> None: - boot() - - if __name__ == "__main__": - main() + boot() From a5178b486598f7893c0ad3df67e15284be57cc89 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:31:05 +0100 Subject: [PATCH 16/75] refactor: only import the necessary UI --- tilia/boot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tilia/boot.py b/tilia/boot.py index 5c392e552..c7a195fe5 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -13,8 +13,6 @@ from tilia.file.autosave import AutoSaver from tilia.log import logger from tilia.media.player import QtAudioPlayer -from tilia.ui.cli.ui import CLI -from tilia.ui.qtui import QtUI, TiliaMainWindow from tilia.undo_manager import UndoManager app = None @@ -94,9 +92,14 @@ def setup_logic(autosaver=True): def setup_ui(q_application: QApplication, interface: str): if interface == "qt": + from tilia.ui.qtui import QtUI, TiliaMainWindow + mw = TiliaMainWindow() return QtUI(q_application, mw) + elif interface == "cli": + from tilia.ui.cli.ui import CLI + return CLI() From 2aba357f9ce7433256e1ed40452b7d995b6a9329 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:56:05 +0100 Subject: [PATCH 17/75] docs: update deps --- CONTRIBUTING.md | 9 ++++++++- README.md | 18 +++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a0335265..f38a04887 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,4 +28,11 @@ If you find a bug, [check](https://github.com/TimeLineAnnotator/desktop/issues) # Suggesting features or enhancements We are very much looking for ideas of new features for TiLiA. If you would like to suggest one, [check](https://github.com/TimeLineAnnotator/desktop/issues) to see if your feature has already been requested. If not, [open a new one](https://github.com/TimeLineAnnotator/desktop/issues/new) detailing your suggestion. -You need Python 3.11 to build and test TiLiA. You will also need to have ffmpeg installed to use the export and convert audio features. +You need Python >=3.10 to build and test TiLiA. You will also need to have ffmpeg installed to use the export and convert audio features. + +# Developing for TiLiA +For a better development experience, we recommend the installation of a few more packages: +``` +pip install --group dev --group testing +pre-commit install +``` diff --git a/README.md b/README.md index a7fb226ae..b4eae721e 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,15 @@ TiLiA can be also run and build from source. ### Prerequisites Before you start, you will need: -- Python 3.11 or later. You can get it at the [Python website](https://www.python.org/downloads/). -- `pip` to install dependencies. +- Python 3.10 - 3.12. Download from the [Python website](https://www.python.org/downloads/). (Support for Python 3.13 is variable due to the dependencies used.) +-
+ Other pre-requisites + + | Package | Installation | Notes | + | --- | --- | --- | + | `pip` | `python -m ensurepip --upgrade` | `pip` should come with your Python installation | + | `git` | Download [here](https://git-scm.com/install) | Not necessary for a direct download from this repository | +
### Note for Linux users Users have reported dependency issues when running TiLiA on Linux (see [#370](https://github.com/TimeLineAnnotator/desktop/issues/370) and [#371](https://github.com/TimeLineAnnotator/desktop/issues/371)). @@ -76,7 +83,7 @@ cd tilia-desktop Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps. Failure to do so is likely to cause issues with dependencies. -Install python dependencies with: +Install TiLiA and its dependencies with: ``` pip install -e . ``` @@ -84,10 +91,11 @@ On Linux, some additional Qt dependencies are required: ``` sudo apt install libnss3 libasound libxkbfile1 libpulse0 ``` +| n.b.: the specific names of these packages varies based on your Linux distribution. -To run TiLiA from source, use: +To run TiLiA from source, run: ``` -python -m tilia +tilia-desktop ``` TiLiA also offers a CLI mode, which can be run with: From 64eaf57397d062b06707007e2749afebd3771313 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Sat, 6 Dec 2025 11:44:19 +0100 Subject: [PATCH 18/75] docs: some spelling --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b4eae721e..0c54e1489 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ Here are some examples TiLiA visualizations: ## Current features -- 6 kinds of timelines +- 7 kinds of timelines - AudioWave: visualize audio files through bars that represent changes in amplitude - Beat: beat and measure markers with support to numbering - Harmony: Roman numeral and chord symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords - - Hierarchy: nested and leveled units organized in arbitrally complex hierarchical structures - - Marker: simple, labeled markers to indicate discrete events + - Hierarchy: nested and levelled units organized in arbitrarily complex hierarchical structures + - Marker: simple, labelled markers to indicate discrete events - PDF: visualize PDF files synced to playback - Score: visualize music scores in custom, to-scale notation or conventional engraving - Controlling playback by clicking on timeline units @@ -41,9 +41,9 @@ Here are some examples TiLiA visualizations: - Command-line interface (see the CLI [help page](docs/cli-tutorial.md) for more information) ## How to install and use -TiLiA has releases for Windows, macOS and Linux. Instructions to download and run are [at the website](https://tilia-app.com/help/introduction/). +TiLiA has releases for Windows, macOS and Linux. Instructions to download and run are [on our website](https://tilia-app.com/help/introduction/). -Tutorials on how to use TiLiA can be found at the [website](https://tilia-app.com/help/) or in video [here](https://www.youtube.com/@tilia-app). +Tutorials on how to use TiLiA can be found on our [website](https://tilia-app.com/help/) or in these [videos](https://www.youtube.com/@tilia-app). ## Build or run from source From 15c5b48b6d018ff1d054e33166fc8460c9dbd430 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:35:38 +0100 Subject: [PATCH 19/75] chore: completely remove setup.cfg Apparently the modern way of development is using pyproject instead of setup.cfg... Ideally, we might want to consider moving away from black + flake to ruff too. --- .pre-commit-config.yaml | 19 +++++++++++++------ setup.cfg | 5 ----- 2 files changed, 13 insertions(+), 11 deletions(-) delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6237874a0..6220c0d98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,27 @@ repos: -- repo: https://github.com/psf/black + - repo: https://github.com/psf/black rev: 22.12.0 hooks: - - id: black + - id: black -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-added-large-files - args: ['--maxkb=1000'] + args: ["--maxkb=1000"] - id: debug-statements - id: check-yaml -- repo: https://github.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 7.1.0 hooks: - - id: flake8 + - id: flake8 + args: + [ + "--max-line-length=88", + "--per-file-ignores=__init__.py:F401", + "--extend-ignore=E203,E501", + "--exclude=tilia/dev", + ] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1580f0938..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 -per-file-ignores = __init__.py: F401 -extend-ignore = E203,E501 -exclude = tilia/dev From 4b57ee5f461efdf779020e71dc677a74692d25b9 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:01:29 +0100 Subject: [PATCH 20/75] refactor: use common load env method --- tests/conftest.py | 8 ++------ tilia/boot.py | 8 ++------ tilia/dirs.py | 5 +---- tilia/utils.py | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ff4838888..abff9df44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Literal -import dotenv import pytest from PySide6.QtCore import QSettings from PySide6.QtWidgets import QApplication @@ -12,7 +11,6 @@ import tilia.constants as constants_module import tilia.log as logging_module import tilia.settings as settings_module -from tilia.dirs import PROJECT_ROOT from tilia.media.player.base import MediaTimeChangeReason from tilia.app import App from tilia.boot import setup_logic @@ -29,6 +27,7 @@ from tilia.ui.windows import WindowKind from tilia.requests.get import reset as reset_get from tilia.requests.post import reset as reset_post +from tilia.utils import load_dotenv try: # icecream is a replacement for print() @@ -51,10 +50,7 @@ "tests.timelines.score.fixtures", ] -dotenv_path = PROJECT_ROOT / ".env" -success = dotenv.load_dotenv(dotenv_path) -if not success: - raise FileNotFoundError(f"No .env file found at {dotenv_path.resolve()}") +load_dotenv() class TiliaErrors: diff --git a/tilia/boot.py b/tilia/boot.py index c7a195fe5..b09c62a6a 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -3,12 +3,11 @@ import sys import traceback -import dotenv from PySide6.QtWidgets import QApplication from tilia.app import App from tilia.clipboard import Clipboard -from tilia.dirs import PROJECT_ROOT, setup_dirs +from tilia.dirs import setup_dirs from tilia.file.file_manager import FileManager from tilia.file.autosave import AutoSaver from tilia.log import logger @@ -33,10 +32,7 @@ def handle_expection(type, value, tb): def boot(): sys.excepthook = handle_expection - dotenv_path = PROJECT_ROOT / ".env" - success = dotenv.load_dotenv(dotenv_path) - if not success: - raise FileNotFoundError(f"No .env file found at {dotenv_path.resolve()}") + args = setup_parser() setup_dirs() logger.setup() diff --git a/tilia/dirs.py b/tilia/dirs.py index f2f2650fe..4ed49214f 100644 --- a/tilia/dirs.py +++ b/tilia/dirs.py @@ -12,10 +12,7 @@ _USER_DATA_DIR = Path( platformdirs.user_data_dir(tilia.constants.APP_NAME, roaming=True) ) -data_path = _SITE_DATA_DIR -PROJECT_ROOT = Path(tilia.__file__).parents[1] -TILIA_DIR = Path(tilia.__file__).parent -IMG_DIR = Path(TILIA_DIR, "ui", "img") +IMG_DIR = Path(__file__).parent / "ui" / "img" def setup_data_dir() -> Path: diff --git a/tilia/utils.py b/tilia/utils.py index 0753f03e2..9765f5827 100644 --- a/tilia/utils.py +++ b/tilia/utils.py @@ -21,3 +21,25 @@ def open_with_os(path: Path) -> None: subprocess.Popen(["open", path.resolve()], shell=True) else: raise OSError(f"Unsupported platform: {sys.platform}") + + +def load_dotenv() -> None: + dotenv_path = Path(__file__).parents[1] / ".env" + if os.path.exists(dotenv_path): + import dotenv + + success = dotenv.load_dotenv(dotenv_path) + if not success: + raise FileNotFoundError( + f"Error loading .env file from {dotenv_path.resolve()}" + ) + else: # setup some basic values + os.environ["LOG_REQUESTS"] = "1" + os.environ[ + "EXCLUDE_FROM_LOG" + ] = "TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE" + if not os.environ.get("ENVIRONMENT"): + os.environ["ENVIRONMENT"] = "dev" + + +load_dotenv() From 0b4a9e17cd820e199b8b571d092479d7270267f7 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:48:12 +0100 Subject: [PATCH 21/75] refactor: defaults to dev see 1bd7d24 --- tilia/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilia/log.py b/tilia/log.py index 198371585..8f61bad41 100644 --- a/tilia/log.py +++ b/tilia/log.py @@ -41,7 +41,7 @@ def __init__(self): self._dump_count = count() def setup(self): - match (env := os.environ.get("ENVIRONMENT", "prod")): + match (env := os.environ.get("ENVIRONMENT")): case "test": self.disabled = True self.setup_sentry(env) From aa7bda56c619fe513a684261757d3915fc8972aa Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:53:45 +0100 Subject: [PATCH 22/75] ci: update dependency for toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 179be8f7d..c85d906e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ dependencies = [ "PySide6_Essentials>=6.8.0", "sentry-sdk>=2.21.0", "soundfile~=0.13.1", - "TiLiA" + "TiLiA", + "tomli; python_version < '3.11'" ] [[project.authors]] From dbc44562f7fb15cf91990114052a06d0aba013e1 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:32:07 +0100 Subject: [PATCH 23/75] chore: typos --- tilia/app.py | 2 +- tilia/boot.py | 4 ++-- tilia/errors.py | 4 ++-- tilia/file/file_manager.py | 9 ++++++--- tilia/media/player/youtube.py | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tilia/app.py b/tilia/app.py index 8467aa803..fd8c5d970 100644 --- a/tilia/app.py +++ b/tilia/app.py @@ -257,7 +257,7 @@ def on_restore_state(self, state: dict) -> None: def recover_to_state(self, state: dict) -> None: """ - Clears the app and attemps to restore the given state. + Clears the app and attempts to restore the given state. Unlike `on_restore_state` this will crash if an error occurs during the restoration. This is meant to be used after an exception occurred, so if the diff --git a/tilia/boot.py b/tilia/boot.py index b09c62a6a..bcc8088a2 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -18,7 +18,7 @@ ui = None -def handle_expection(type, value, tb): +def handle_exception(type, value, tb): exc_message = "".join(traceback.format_exception(type, value, tb)) if ui: ui.show_crash_dialog(exc_message) @@ -31,7 +31,7 @@ def handle_expection(type, value, tb): def boot(): - sys.excepthook = handle_expection + sys.excepthook = handle_exception args = setup_parser() setup_dirs() diff --git a/tilia/errors.py b/tilia/errors.py index 0373df743..3d04c7dd6 100644 --- a/tilia/errors.py +++ b/tilia/errors.py @@ -24,7 +24,7 @@ class Error(NamedTuple): CSV_IMPORT_FAILED = Error("CSV import failed", "Import failed:\n{}") CSV_IMPORT_SUCCESS_ERRORS = Error( "CSV import", - "Import was successful, but some components may not have been imported.\nThe following errors occured:\n{}", + "Import was successful, but some components may not have been imported.\nThe following errors occurred:\n{}", ) CREATE_TIMELINE_WITHOUT_MEDIA = Error( "Create timeline error", "Cannot create timeline with no media loaded." @@ -75,7 +75,7 @@ class Error(NamedTuple): COMPONENTS_COPY_ERROR = Error("Copy components error", "{}") COMPONENTS_LOAD_ERROR = Error( "Load components error", - "Some components were not loaded. The following errors occured:\n{}", + "Some components were not loaded. The following errors occurred:\n{}", ) COMPONENTS_PASTE_ERROR = Error("Paste components error", "{}") FILE_NOT_FOUND = Error( diff --git a/tilia/file/file_manager.py b/tilia/file/file_manager.py index 3a0132202..1d72d8fef 100644 --- a/tilia/file/file_manager.py +++ b/tilia/file/file_manager.py @@ -2,8 +2,11 @@ from pathlib import Path import json -from tilia.exceptions import MediaMetadataFieldNotFound, MediaMetadataFieldAlreadyExists -import tilia.exceptions +from tilia.exceptions import ( + MediaMetadataFieldAlreadyExists, + MediaMetadataFieldNotFound, + NoReplyToRequest, +) from tilia.file.common import are_tilia_data_equal, write_tilia_file_to_disk from tilia.requests import listen, Post, Get, serve, get, post from tilia.file.tilia_file import TiliaFile, validate_tla_data @@ -219,7 +222,7 @@ def save(self, data: dict, path: Path | str): try: geometry, window_state = get(Get.WINDOW_GEOMETRY), get(Get.WINDOW_STATE) - except tilia.exceptions.NoReplyToRequest: + except NoReplyToRequest: geometry, window_state = None, None settings.update_recent_files(path, geometry, window_state) diff --git a/tilia/media/player/youtube.py b/tilia/media/player/youtube.py index c14688dd7..0cd2bd20b 100644 --- a/tilia/media/player/youtube.py +++ b/tilia/media/player/youtube.py @@ -118,7 +118,7 @@ def load_media( initial_duration: float = 0.0, ): """ - Returns True if media loading has *started* succesfully, False otherwise. + Returns True if media loading has *started* successfully, False otherwise. Loading is asynchronous, and self.on_media_load_done will be called when it is completed. If initial_duration is provided, it will be available when returning. From 79ab3231f9315552c6111a513f929014d04c2c89 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:34:32 +0100 Subject: [PATCH 24/75] refactor: reduce method calls env is unlikely to change (at least in deployment) after loading --- tilia/requests/post.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tilia/requests/post.py b/tilia/requests/post.py index 85a85a1a0..d0ace8aa4 100644 --- a/tilia/requests/post.py +++ b/tilia/requests/post.py @@ -104,11 +104,10 @@ class Post(Enum): ] = weakref.WeakKeyDictionary() -def _get_posts_excluded_from_log() -> list[Post]: - result = [] - for name in os.environ.get("EXCLUDE_FROM_LOG", "").split(";"): - result.append(Post[name]) - return result +EXCLUDED_POSTS = [ + Post[post] for post in os.environ.get("EXCLUDE_FROM_LOG", "").split(";") +] +LOG_REQUESTS = os.environ.get("LOG_REQUESTS", 0) def _log_post(post, *args, **kwargs): @@ -124,7 +123,7 @@ def _log_post(post, *args, **kwargs): def post(post: Post, *args, **kwargs) -> None: - if os.environ.get("LOG_REQUESTS", 0) and post not in _get_posts_excluded_from_log(): + if LOG_REQUESTS and post not in EXCLUDED_POSTS: _log_post(post, args, kwargs) # Returning a result is an experimental feature. # This can be very useful to check if the request was successful. From 86949ad30918a6bc2151b20d5fda0ff0bc4ca2c5 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:41:24 +0100 Subject: [PATCH 25/75] fix: locating metadata we are not packaging the toml file in the deployed --- tilia/constants.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/tilia/constants.py b/tilia/constants.py index 7fc47866e..845aff17e 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -1,19 +1,38 @@ -try: - from tomllib import load -except ModuleNotFoundError: - from tomli import load +from importlib import metadata from pathlib import Path +import re -with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f: - setupcfg = load(f).get("project", {}) +if (toml := Path(__file__).parent.parent / "pyproject.toml").exists(): + from sys import version_info + + if version_info >= (3, 11): + from tomllib import load + else: + from tomli import load + + with open(toml, "rb") as f: + setupcfg = load(f).get("project", {}) + AUTHOR = setupcfg.get("authors", [{"name": ""}])[0]["name"] + EMAIL = setupcfg.get("authors", [{"email": ""}])[0]["email"] + +else: + setupcfg = metadata.metadata("TiLiA").json.copy() + + AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1) + EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1) + if "urls" not in setupcfg: + setupcfg["urls"] = {} + for url in setupcfg.get("project_url", {}): + k, _, v = url.partition(", ") + setupcfg["urls"][k] = v + setupcfg["description"] = setupcfg.get("summary", "") APP_NAME = setupcfg.get("name", "") -AUTHOR = setupcfg.get("authors", [{"name": ""}])[0]["name"] -VERSION = setupcfg.get("version", "beta") +VERSION = setupcfg.get("version", "0.0.0") YEAR = "2022-2025" FILE_EXTENSION = "tla" -EMAIL_URL = "mailto:" + setupcfg.get("authors", [{"email": ""}])[0]["email"] +EMAIL_URL = "mailto:" + EMAIL GITHUB_URL = setupcfg.get("urls", {}).get("Repository", "") WEBSITE_URL = setupcfg.get("urls", {}).get("Homepage", "") From 2f81222e6f21a8cd7b2ba77fe0fdeb2643bf480d Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:46:36 +0100 Subject: [PATCH 26/75] refactor: read license file in constants --- tilia/constants.py | 4 ++++ tilia/ui/windows/about.py | 14 ++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tilia/constants.py b/tilia/constants.py index 845aff17e..2a5702ce0 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -45,3 +45,7 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. """ + +with open(Path(__file__).parent.parent / "LICENSE", encoding="utf-8") as f: + LICENSE_TEXT = f.read() +LICENSE = re.split("How to Apply These Terms to Your New Programs", LICENSE_TEXT)[0] diff --git a/tilia/ui/windows/about.py b/tilia/ui/windows/about.py index a2b3a3106..dd4e28aeb 100644 --- a/tilia/ui/windows/about.py +++ b/tilia/ui/windows/about.py @@ -1,4 +1,3 @@ -from pathlib import Path from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -8,7 +7,7 @@ QScrollArea, QVBoxLayout, ) -from re import split, sub +from re import sub import tilia.constants @@ -84,14 +83,9 @@ def __init__(self, parent): notice = QLabel(tilia.constants.NOTICE) notice.setWordWrap(True) - with open( - Path(__file__).parent.parent.parent.parent / "LICENSE", encoding="utf-8" - ) as license_file: - license_text = split( - "How to Apply These Terms to Your New Programs", license_file.read() - )[0] - - formatted_text = f'
{license_text}
' + formatted_text = ( + f'
{tilia.constants.LICENSE}
' + ) text_with_links = sub( "]+)>", lambda y: f'{y[0][1:-1]}', From f4838f515bc0d460b8cf04df24918d104d6778a8 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:51:38 +0100 Subject: [PATCH 27/75] fix: dir in deployment cannot be chdir'ed into --- tilia/dirs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tilia/dirs.py b/tilia/dirs.py index 4ed49214f..e2956efd0 100644 --- a/tilia/dirs.py +++ b/tilia/dirs.py @@ -37,8 +37,6 @@ def setup_logs_path(data_dir): def setup_dirs() -> None: - os.chdir(os.path.dirname(__file__)) - data_dir = setup_data_dir() global autosaves_path, logs_path From 6cf9f8410535c663adfdbbe18a54d8ef0811bc22 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:22:48 +0100 Subject: [PATCH 28/75] chore: technical difference in its deployed state, having the extra packages does not extend any of the app's capabilities --- .github/workflows/run-tests.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e7fe91735..1642900cc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip wheel - pip install -e .[testing,ci-tests] + pip install -e . --group testing --group ci-tests - name: List installed packages run: | diff --git a/pyproject.toml b/pyproject.toml index c85d906e6..485cee1c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ tilia = "tilia.main:main" Homepage = "https://tilia-app.com" Repository = "https://github.com/TimeLineAnnotator/desktop" -[project.optional-dependencies] +[dependency-groups] ci-tests = [ "coverage>=7.0.0", "coverage-badge>=1.0.0", From c912eab7234ad55d7af8d238f140bbab43b8bdf0 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:57:25 +0000 Subject: [PATCH 29/75] fix: typos --- tilia/ui/timelines/collection/import_.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tilia/ui/timelines/collection/import_.py b/tilia/ui/timelines/collection/import_.py index 06c1089b2..eac6aa614 100644 --- a/tilia/ui/timelines/collection/import_.py +++ b/tilia/ui/timelines/collection/import_.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING import tilia.errors -import tilia.parsers from tilia.requests import get, Get from tilia.timelines.timeline_kinds import TimelineKind as TlKind from tilia.ui.dialogs.by_time_or_by_measure import ByTimeOrByMeasure @@ -47,7 +46,7 @@ def _on_import_to_timeline( success, path = get( Get.FROM_USER_FILE_PATH, "Import components", - ["musicXML files (*.musicxml *.mxl, *.xml)"], + ["musicXML files (*.musicxml *.mxl *.xml)"], ) else: From b78bf9a63bbd4ac8567e1999458d69baabe13664 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:15:38 +0100 Subject: [PATCH 30/75] fix: more secure dotenv slightly more secure way of fixing 79f6b55. .tilia.env now stores the basic env vars. create another .env file and to store dev specific vars privately. --- .env => .tilia.env | 0 tilia/utils.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) rename .env => .tilia.env (100%) diff --git a/.env b/.tilia.env similarity index 100% rename from .env rename to .tilia.env diff --git a/tilia/utils.py b/tilia/utils.py index 9765f5827..b9a674e4d 100644 --- a/tilia/utils.py +++ b/tilia/utils.py @@ -24,15 +24,16 @@ def open_with_os(path: Path) -> None: def load_dotenv() -> None: - dotenv_path = Path(__file__).parents[1] / ".env" - if os.path.exists(dotenv_path): + dotenv_paths = [ + fn for fn in os.listdir(Path(__file__).parents[1]) if fn.endswith(".env") + ] + + if dotenv_paths: import dotenv - success = dotenv.load_dotenv(dotenv_path) - if not success: - raise FileNotFoundError( - f"Error loading .env file from {dotenv_path.resolve()}" - ) + for p in dotenv_paths: + dotenv.load_dotenv(p) + else: # setup some basic values os.environ["LOG_REQUESTS"] = "1" os.environ[ From 93f0a8150ba4f1a6461dab697a1bbe9600a919c8 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:12:19 +0100 Subject: [PATCH 31/75] build: nuitka to produce build, install and start up a new application: ``` pip install -e . tilia-desktop ``` --- .github/workflows/build.yml | 165 +++++++++++++------- .github/workflows/run-tests.yml | 4 + .pre-commit-config.yaml | 2 +- pyproject.toml | 62 +++++++- scripts/deploy.py | 261 ++++++++++++++++++++++++++++++++ scripts/linux-setup.sh | 93 ++++++++++++ tests/conftest.py | 7 +- tilia.nuitka-package.config.yml | 50 ++++++ tilia.spec | 101 ------------ tilia/__main__.py | 56 ++++++- tilia/boot.py | 4 + tilia/constants.py | 2 +- tilia/utils.py | 11 +- 13 files changed, 640 insertions(+), 178 deletions(-) create mode 100644 scripts/deploy.py create mode 100644 scripts/linux-setup.sh create mode 100644 tilia.nuitka-package.config.yml delete mode 100644 tilia.spec diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f1ff3790..08f151f12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,85 +1,138 @@ -name: Build with pyinstaller +name: Build TiLiA on: workflow_dispatch: push: tags: - - 'v*' + - "v*" jobs: build: - name: Build with pyinstaller + name: Build with Nuitka + continue-on-error: true runs-on: ${{ matrix.os }} + permissions: + contents: read + outputs: + linux-build: ${{ steps.set-path.outputs.path }} strategy: matrix: - include: - - os: windows-latest - CMD_BUILD: pyinstaller tilia.spec - OUT_FILE_OS: win - OUT_FILE_EXTENSION: .exe - ASSET_MIME: application/vnd.microsoft.portable-executable - - - os: macos-13 - CMD_BUILD: > - pyinstaller tilia.spec && - cd dist && - mv tilia-$APP_VERSION-mac.app tilia-$APP_VERSION-mac-intel.app && - zip -r9 tilia-$APP_VERSION-mac-intel.zip tilia-$APP_VERSION-mac-intel.app - OUT_FILE_OS: mac-intel - OUT_FILE_EXTENSION: .zip - ASSET_MIME: application/zip - - - os: macos-14 - CMD_BUILD: > - pyinstaller tilia.spec && - cd dist && - mv tilia-$APP_VERSION-mac.app tilia-$APP_VERSION-mac-silicon.app && - zip -r9 tilia-$APP_VERSION-mac-silicon.zip tilia-$APP_VERSION-mac-silicon.app - OUT_FILE_OS: mac-silicon - OUT_FILE_EXTENSION: .zip - ASSET_MIME: application/zip - - - os: ubuntu-latest - CMD_BUILD: > - pyinstaller tilia.spec - OUT_FILE_OS: linux - OUT_FILE_EXTENSION: - ASSET_MIME: application/octet-stream + os: [ + macos-latest, + macos-15-intel, + ubuntu-22.04, + windows-latest + ] + env: + QT_DEBUG_PLUGINS: 1 + QT_QPA_PLATFORM: offscreen steps: - uses: actions/checkout@v4 - - name: Extract package version - id: extract_version - shell: pwsh - run: | - $version = (Select-String '^version =' setup.cfg).Line -split ' = ' | Select-Object -Last 1 - echo "APP_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append - - - name: Set output file name - shell: pwsh - run: | - $out_file_name = "tilia-${{env.APP_VERSION}}-${{matrix.OUT_FILE_OS}}${{matrix.OUT_FILE_EXTENSION}}" - echo "OUT_FILE_NAME=$out_file_name" | Out-File -FilePath $env:GITHUB_ENV -Append - - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: 3.11 - cache: 'pip' + cache: "pip" + + - name: Setup additional Linux dependencies + if: runner.os == 'Linux' + run: | + chmod +x ./scripts/linux-setup.sh + ./scripts/linux-setup.sh + + - name: Remove problematic brew libs (see Nuitka/Nuitka#2853) + if: runner.os == 'macOS' && runner.arch != 'ARM64' + run: | + brew remove --force --ignore-dependencies openssl@3 + brew cleanup openssl@3 + brew install coreutils - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools wheel - pip install -e . - pip install pyinstaller + python -m pip install --upgrade pip + pip install -e . --group build - name: Build executable + id: build-exe shell: bash - run: ${{matrix.CMD_BUILD}} + run: | + echo "python scripts/deploy.py ${{github.ref_name}} ${{matrix.os}}" | bash + + - name: Test executable [mac] + if: runner.os == 'macOS' + shell: bash + run: | + echo "Checking dependencies" + echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + + - name: Test executable [Windows] + if: runner.os == 'Windows' + shell: bash + run: | + echo "Checking dependencies" + echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + + - name: Test executable [Linux] + id: set-path + if: runner.os == 'Linux' + shell: bash + run: | + echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT" + echo "Checking dependencies" + echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true - name: Upload executable uses: actions/upload-artifact@v4 with: - name: ${{env.OUT_FILE_NAME}} - path: dist/${{env.OUT_FILE_NAME}} + name: ${{ steps.build-exe.outputs.out-filename }} + path: build/*/exe + retention-days: 1 + + deploy: + name: Create release + continue-on-error: true + needs: build + runs-on: "ubuntu-latest" + permissions: + contents: write + env: + QT_DEBUG_PLUGINS: 1 + QT_QPA_PLATFORM: offscreen + steps: + - uses: actions/checkout@v4 + + - name: Download executable + uses: actions/download-artifact@v4 + with: + path: build/ + pattern: 'TiLiA-v*' + merge-multiple: true + + - name: Test Linux still works in a clean environment + continue-on-error: true + id: test-linux + run: | + if [ -e build/ubuntu/exe/${{ needs.build.outputs.linux-build }} ]; + then + chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }}; + if echo "timeout 30 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash; + then + echo "Linux works in a clean environment!"; + else + echo "Linux didn't work... Check libraries with previous step?"; + fi; + else + echo "file not found!"; + fi + ls build -ltraR |egrep -v '\.$|\.\.|\.:|\.\/|total|^d' | sed '/^$/d' || true + + - name: Upload + uses: "softprops/action-gh-release@v2" + with: + tag_name: ${{github.ref_name}} + make_latest: ${{github.event_name == 'push'}} + generate_release_notes: true + files: | + build/*/exe/TiLiA-v* diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1642900cc..876666af3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -79,3 +79,7 @@ jobs: - name: Generate Coverage Report run: | coverage report -m + + - name: Check Program + shell: bash + run: timeout 10 tilia-desktop || true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6220c0d98..dd2837f4f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: args: [ "--max-line-length=88", - "--per-file-ignores=__init__.py:F401", + "--per-file-ignores=__init__.py:F401,tilia/__main__.py:E402,tilia/boot.py:E402,tests/conftest.py:E402", "--extend-ignore=E203,E501", "--exclude=tilia/dev", ] diff --git a/pyproject.toml b/pyproject.toml index 485cee1c5..0e2306482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,14 @@ [build-system] requires = [ - "setuptools>=78.1.1", - "wheel", + "Nuitka", + "setuptools", ] build-backend = "setuptools.build_meta" [project] name = "TiLiA" -version = "0.5.15" +version = "0.5.15.0" description = "A GUI for creating and visualizing annotations over audio and video files." -license = "GPL-3.0" -license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.10,<3.14" dependencies = [ @@ -38,20 +36,33 @@ name = "Felipe Defensor et al." email = "tilia@tilia-app.com" [project.gui-scripts] -tilia = "tilia.main:main" +tilia-desktop = "tilia.__main__:boot" [project.urls] Homepage = "https://tilia-app.com" Repository = "https://github.com/TimeLineAnnotator/desktop" [dependency-groups] +build = [ + "build", + "ImageIO; sys_platform == 'darwin'", + "Nuitka >= 4.0", + "ordered-set==4.1.0", + "pyyaml", + "setuptools; python_version >= '3.12'", + "wheel==0.38.4", + "zstandard==0.20.0" +] ci-tests = [ "coverage>=7.0.0", "coverage-badge>=1.0.0", "flake8>=7.0.0", "pytest-cov>=6.0.0", ] -dev = ["icecream>=2.1.0"] +dev = [ + "icecream>=2.1.0", + "pre-commit", +] testing = [ "pytest>=8.0.0", "pytest-env>=1.1.5", @@ -61,8 +72,45 @@ testing = [ source = ["tilia"] omit = ["tilia/dev/*"] +[tool.nuitka] +assume-yes-for-downloads = true +enable-plugins = ["pyside6"] +include-qt-plugins = ["multimedia"] +nofollow-import-to = ["*.tests", "*.test"] +noinclude-qt-translations = true +onefile-tempdir-spec = "{CACHE_DIR}/{PRODUCT}/v{VERSION}" +remove-output = true +report = "compilation-report.xml" +user-package-configuration-file = "tilia.nuitka-package.config.yml" +deployment = true + [tool.pytest.ini_options] env = [ "ENVIRONMENT=test", "QT_QPA_PLATFORM=offscreen", ] + +[tool.setuptools.packages.find] +where = ["."] +exclude = [ + "build", "build.*", + "dist", + "docs", "docs.*", + "paper", "paper.*", + "scripts", + "tests", "tests.*", + "venv.*", "venv" +] + +[tool.setuptools.package-data] +tilia = [ + "../.tilia.env", + "../tilia.nuitka-package.config.yml", # because we build from sdist + "../LICENSE", + "ui/img/*", + "ui/fonts/*", + "media/player/youtube.html", + "media/player/youtube.css", + "parsers/score/svg_maker.html", + "parsers/score/timewise_to_partwise.xsl", +] diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100644 index 000000000..152a3c0b8 --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,261 @@ +from colorama import Fore +import dotenv +from enum import Enum +from lxml import etree +from nuitka.distutils.DistutilsCommands import build as n_build +import os +from pathlib import Path +from subprocess import check_call +import sys +import tarfile +import traceback + + +ref_name = "" +build_os = "" +buildlib = Path(__file__).parents[1] / "build" +toml_file = Path(__file__).parents[1] / "pyproject.toml" +pkg_cfg = "tilia.nuitka-package.config.yml" +outdir = Path() +out_filename = "" + +if not toml_file.exists(): + options = {} +else: + if sys.version_info >= (3, 11): + from tomllib import load + else: + from tomli import load + + with open(toml_file, "rb") as f: + options = load(f) + + +class P(Enum): + CMD = Fore.BLUE + ERROR = Fore.RED + OK = Fore.GREEN + + +def _print(text: list[str | list[str]], p_type: P | None = None): + if not text: + return + formatted_text = "\n".join([t.__str__() for t in text]) + if p_type: + formatted_text = p_type.value + formatted_text + Fore.RESET + sys.stdout.write(formatted_text + "\n") + + +def _handle_inputs(): + assert len(sys.argv) == 3, "Incorrect number of inputs" + global ref_name, build_os, outdir + ref_name = sys.argv[1] + build_os = "-".join( + [ + x + for x in sys.argv[2].split("-") + if not x.replace(".", "", 1).isdigit() and x != "latest" + ] + ) + outdir = buildlib / build_os + + +def _get_nuitka_toml() -> list[str]: + toml_cmds = [] + for option, value in options.get("tool", {}).get("nuitka", {}).items(): + toml_cmds.extend(n_build._parseOptionsEntry(option, value)) + return toml_cmds + + +def _set_out_filename(name: str, version: str): + def _clean_version(v: str) -> list[str]: + return v.strip("v ").split(".") + + global out_filename + if _clean_version(version) == _clean_version(ref_name): + out_filename = f"{name}-v{version}-{build_os}" + else: + out_filename = f"{name}-v{version}-{ref_name}-{build_os}" + + +def _get_exe_cmd() -> list[str]: + name = options.get("project", {}).get("name", "TiLiA") + version = options.get("project", {}).get("version", "0") + _set_out_filename(name, version) + icon_path = Path(__file__).parents[1] / "tilia" / "ui" / "img" / "main_icon.ico" + exe_args = [ + sys.executable, + "-m", + "nuitka", + f"--output-dir={outdir}/exe", + f"--product-name={name}", + f"--file-version={version}", + f"--output-filename={out_filename}", + # "--onefile-tempdir-spec={CACHE_DIR}/{PRODUCT}/{VERSION}", + f"--macos-app-icon={icon_path}", + "--macos-app-mode=gui", + f"--macos-app-version={version}", + "--windows-console-mode=attach", + f"--windows-icon-from-ico={icon_path}", + f"--linux-icon={icon_path}", + ] + if "macos" in build_os: + exe_args.append("--mode=app") + else: + exe_args.append("--mode=onefile") + + return exe_args + + +def _get_sdist() -> Path: + for f in outdir.iterdir(): + if "".join(f.suffixes[-2:]) == ".tar.gz": + return f + raise Exception(f"Could not find sdist in {outdir}:", [*outdir.iterdir()]) + + +def _get_main_file() -> Path: + main = _create_lib() + _update_yml() + return main + + +def _create_lib() -> Path: + sdist = _get_sdist() + base = ".".join(sdist.name.split(".")[:-2]) + tilia = f"{base}/tilia" + lib = outdir / "Lib" + + ext_data = [ + f"{base}/{e[3:]}" + for e in options.get("tool", {}) + .get("setuptools", {}) + .get("package-data", {}) + .get("tilia", []) + if e.split("/")[0] == ".." + ] + + with tarfile.open(sdist) as f: + main = f"{tilia}/__main__.py" + assert main in f.getnames(), f"Could not locate {main}" + f.extractall( + lib, + filter=lambda x, _: ( + x + if x.name.startswith(tilia) + or x.name.startswith("TiLiA.egg-info") + or x.name in ext_data + else None + ), + ) + + os.chdir(lib / base) + return lib / tilia + + +def _update_yml(): + if not Path(pkg_cfg).exists(): + return + + import yaml + + with open(pkg_cfg) as f: + yml = yaml.safe_load(f) + + yml.append( + { + "module-name": "tilia", + "data-files": [ + { + "patterns": [ + e + for e in options.get("tool", {}) + .get("setuptools", {}) + .get("package-data", {}) + .get("tilia", []) + if pkg_cfg not in e + ] + }, + {"include-metadata": ["TiLiA"]}, + ], + } + ) + + with open(pkg_cfg, "w") as f: + yaml.dump(yml, f) + + +def _build_sdist(): + sdist_cmd = [ + sys.executable, + "-m", + "build", + "--no-isolation", + "--verbose", + "--sdist", + f"--outdir={outdir.as_posix()}", + ] + + _print(["Building sdist with command:", sdist_cmd], P.CMD) + check_call(sdist_cmd) + + +def _build_exe(): + main_file = _get_main_file() + exe_cmd = _get_exe_cmd() + exe_cmd.extend(_get_nuitka_toml()) + exe_cmd.append(main_file.as_posix()) + + _print(["Building exe with command:", exe_cmd], P.CMD) + check_call(exe_cmd) + _print(["Build complete!"], P.OK) + _print(["Compilation report:"]) + _print( + [ + etree.tostring( + etree.parse(main_file.parent / "compilation-report.xml"), + pretty_print=True, + encoding=str, + ) + ] + ) + + +def build(): + _handle_inputs() + old_env_var = dotenv.dotenv_values(".tilia.env").get("ENVIRONMENT", "") + dotenv.set_key(".tilia.env", "ENVIRONMENT", "prod") + if buildlib.exists(): + _print(["Cleaning build folder..."], P.ERROR) + for root, dirs, files in os.walk(buildlib, False): + r = Path(root) + _print([f"\t~{r}"]) + for f in files: + os.unlink(r / f) + for d in dirs: + os.rmdir(r / d) + os.rmdir(buildlib) + + old_dir = os.getcwd() + try: + _build_sdist() + _build_exe() + if os.environ.get("GITHUB_OUTPUT"): + if "mac" in build_os: + global outdir + outdir = outdir / "tilia.app" / "Contents" / "MacOS" + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"out-filepath={(outdir / 'exe' / out_filename).as_posix()}\n") + f.write(f"out-filename={out_filename}\n") + os.chdir(old_dir) + dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var) + except Exception as e: + _print(["Build failed!", e.__str__()], P.ERROR) + _print([traceback.format_exc()]) + os.chdir(old_dir) + dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var) + raise SystemExit(1) from e + + +if __name__ == "__main__": + build() diff --git a/scripts/linux-setup.sh b/scripts/linux-setup.sh new file mode 100644 index 000000000..4effae2be --- /dev/null +++ b/scripts/linux-setup.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +echo "Setup Linux build environment" +trap 'echo Setup failed; exit 1' ERR + +df -h . + +########################################################################## +# GET DEPENDENCIES +########################################################################## + +apt_packages=( + coreutils + curl + gawk + git + lcov + libasound2-dev + libcups2-dev + libfontconfig1-dev + libfreetype6-dev + libgcrypt20-dev + libgl1-mesa-dev + libglib2.0-dev + libjack-dev + libnss3-dev + libportmidi-dev + libpulse-dev + librsvg2-dev + libsndfile1-dev + libssl-dev + libtool + make + p7zip-full + sed + unzip + wget + ) + +apt_packages_runtime=( + # Alphabetical order please! + libdbus-1-3 + libegl1-mesa-dev + libgles2-mesa-dev + libodbc2 + libpq-dev + libssl-dev + libxcomposite-dev + libxcursor-dev + libxi-dev + libxkbcommon-x11-0 + libxrandr2 + libxtst-dev + libdrm-dev + libxcb-cursor-dev + libxcb-icccm4 + libxcb-image0 + libxcb-keysyms1 + libxcb-randr0 + libxcb-render-util0 + libxcb-xinerama0 + libxcb-xkb-dev + libxkbcommon-dev + libopengl-dev + libvulkan-dev + ) + +apt_packages_ffmpeg=( + ffmpeg + libavcodec-dev + libavformat-dev + libswscale-dev + ) + +apt_packages_pw_deps=( + libdbus-1-dev + libudev-dev + ) + +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + "${apt_packages[@]}" \ + "${apt_packages_runtime[@]}" \ + "${apt_packages_ffmpeg[@]}" \ + "${apt_packages_pw_deps[@]}" + +########################################################################## +# PIPEWIRE +########################################################################## + +sudo apt-get install pipewire-media-session- wireplumber + +systemctl --user --now enable wireplumber.service diff --git a/tests/conftest.py b/tests/conftest.py index abff9df44..cf8edeba1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,10 @@ from PySide6.QtWidgets import QApplication from colorama import Fore, Style +from tilia.utils import load_dotenv + +load_dotenv() + import tilia.constants as constants_module import tilia.log as logging_module import tilia.settings as settings_module @@ -27,7 +31,6 @@ from tilia.ui.windows import WindowKind from tilia.requests.get import reset as reset_get from tilia.requests.post import reset as reset_post -from tilia.utils import load_dotenv try: # icecream is a replacement for print() @@ -50,8 +53,6 @@ "tests.timelines.score.fixtures", ] -load_dotenv() - class TiliaErrors: def __init__(self): diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml new file mode 100644 index 000000000..b4042cbe0 --- /dev/null +++ b/tilia.nuitka-package.config.yml @@ -0,0 +1,50 @@ +# yamllint disable rule:line-length +# yamllint disable rule:indentation +# yamllint disable rule:comments-indentation +# yamllint disable rule:comments +# too many spelling things, spell-checker: disable +--- +- module-name: "music21" + anti-bloat: + - description: "remove tests" + replacements_plain: + "'mainTest',": "" + "'test',": "" + "from music21.test.testRunner import mainTest": "" + "from music21 import test": "" + - description: "remove module tests" + global_replacements_re: + '(?m)class Test([^(]*)\(unittest.TestCase\):[\d\D]*?(?=^if|^# -|^class|^_[A-Z]|\Z)': "" + "if __name__ == '__main__':([\\d|\\D]*)": "" + "from music21.(.*) import test(.*)": "" + global_replacements_plain: + "import unittest": "" + +- module-name: "executing._pytest_utils" + anti-bloat: + - description: "remove pytest reference" + change_function: + "is_pytest_compatible": "'(lambda : False)'" + when: "not use_pytest" + +- module-name: "sentry_sdk.integrations" + implicit-imports: + - depends: + - "sentry_sdk.integrations.argv" + - "sentry_sdk.integrations.atexit" + - "sentry_sdk.integrations.dedupe" + - "sentry_sdk.integrations.excepthook" + - "sentry_sdk.integrations.logging" + - "sentry_sdk.integrations.modules" + - "sentry_sdk.integrations.stdlib" + - "sentry_sdk.integrations.threading" + anti-bloat: + - description: "fix import of unused integrations" + replacements_plain: + "auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS,": "auto_enabling_integrations=[]," + +- module-name: "tilia.utils" + anti-bloat: + - description: "slightly hacky way of forcing prod if env file is not found" + replacements_plain: + 'os.environ["ENVIRONMENT"] = "dev"': 'os.environ["ENVIRONMENT"] = "prod"' diff --git a/tilia.spec b/tilia.spec deleted file mode 100644 index 41456aa98..000000000 --- a/tilia.spec +++ /dev/null @@ -1,101 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- -import argparse -import platform -import dotenv - -from pathlib import Path - -from tilia.constants import APP_NAME, VERSION - -# Parse build options -parser = argparse.ArgumentParser() -parser.add_argument("--debug", action="store_true") -options = parser.parse_args() - -options = parser.parse_args() - -# Set platform suffix -if platform.system() == 'Windows': - platform_suffix = 'win' -elif platform.system() == 'Darwin': - platform_suffix = 'mac' -elif platform.system() == 'Linux': - platform_suffix = 'linux' -else: - platform_suffix = platform.system() - -# Set enviroment to production -dotenv.set_key(".env", "ENVIRONMENT", "prod") - -# Build executable -a = Analysis( - ["./tilia/main.py"], - pathex=[], - binaries=None, - datas=[ - ("./README.md", "."), - ("./LICENSE", "."), - ("./.env", "."), - ("./setup.cfg", "."), - ("./tilia/ui/img", "./tilia/ui/img/"), - ("./tilia/ui/fonts", "./tilia/ui/fonts/"), - ("./tilia/media/player/youtube.html", "./tilia/media/player/"), - ("./tilia/media/player/youtube.css", "./tilia/media/player/"), - ("./tilia/parsers/score/svg_maker.html", "./tilia/parsers/score/"), - ("./tilia/parsers/score/timewise_to_partwise.xsl", "./tilia/parsers/score/"), - ], - hiddenimports=[], - hookspath=None, - runtime_hooks=None, - excludes=None, -) - -pyz = PYZ(a.pure) - -icon_path = Path("tilia", "ui", "img", "main_icon.ico").resolve().__str__() -executable_basename = f"{APP_NAME.lower()}-{VERSION}-{platform_suffix}" - -if options.debug: - exe = EXE( - pyz, - a.scripts, - name=executable_basename, - console=True, - embed_manifest=True, - exclude_binaries=True, - icon=icon_path, - ) - - coll = COLLECT(exe, a.datas, a.binaries, name="TiLiA") -else: - exe = EXE( - pyz, - a.scripts, - a.datas, - a.binaries, - name=executable_basename, - console=False, - embed_manifest=True, - icon=icon_path, - ) - app = BUNDLE( - exe, - name=f"{executable_basename}.app", - icon=icon_path, - version=VERSION, - info_plist={ - 'NSPrincipalClass': 'NSApplication', - 'NSAppleScriptEnabled': False, - 'CFBundleDocumentTypes': [ - { - 'CFBundleTypeName': 'My File Format', - 'CFBundleTypeIconFile': 'MyFileIcon.icns', - 'LSItemContentTypes': ['com.example.myformat'], - 'LSHandlerRank': 'Owner' - } - ] - }, - ) - -# Reset enviroment -dotenv.set_key(".env", "ENVIRONMENT", "dev") diff --git a/tilia/__main__.py b/tilia/__main__.py index 9cc18b599..544386887 100644 --- a/tilia/__main__.py +++ b/tilia/__main__.py @@ -1,5 +1,57 @@ -from tilia.boot import boot +import sys +from pathlib import Path +import platform +import subprocess +import traceback +import webbrowser + +sys.path[0] = Path(__file__).parents[1].__str__() + + +def deps_debug(exc: ImportError): + root_path = Path( + [*traceback.walk_tb(exc.__traceback__)][0][0].f_code.co_filename + ).parent + lib_path = root_path / "PySide6/qt-plugins/platforms/libqxcb.so" + if not lib_path.exists(): + print( + "Could not find libqxcb.so file.\nDumping all files in tree for debug...\n", + subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"), + ) + raise RuntimeError( + "Could not locate the necessary libraries to run TiLiA." + ) from exc + + _, result = subprocess.getstatusoutput(f"ldd {lib_path.as_posix()}") + if "=> not found" in result: + missing_deps = [] + for line in result.splitlines(): + if "=> not found" in line: + dep = line.strip().rstrip(" => not found") + missing_deps.append(dep) + + from tilia.constants import WEBSITE_URL # noqa: E402 + + distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0] + link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux" + if missing_deps: + print( + f"""TiLiA could not start due to missing system dependencies. +Visit <{link}> for help on installation. +Missing libraries: +{missing_deps}""" + ) + webbrowser.open(link) + + raise RuntimeError("Install the necessary dependencies then restart.") from exc if __name__ == "__main__": - boot() + try: + from tilia.boot import boot # noqa: E402 + + boot() + except ImportError as exc: + if sys.platform != "linux": + raise exc + deps_debug(exc) diff --git a/tilia/boot.py b/tilia/boot.py index bcc8088a2..5cf68ace9 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -5,6 +5,10 @@ from PySide6.QtWidgets import QApplication +from tilia.utils import load_dotenv + +load_dotenv() + from tilia.app import App from tilia.clipboard import Clipboard from tilia.dirs import setup_dirs diff --git a/tilia/constants.py b/tilia/constants.py index 2a5702ce0..34b6ba52e 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -46,6 +46,6 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. """ -with open(Path(__file__).parent.parent / "LICENSE", encoding="utf-8") as f: +with open(Path(__file__).parents[1] / "LICENSE", encoding="utf-8") as f: LICENSE_TEXT = f.read() LICENSE = re.split("How to Apply These Terms to Your New Programs", LICENSE_TEXT)[0] diff --git a/tilia/utils.py b/tilia/utils.py index b9a674e4d..85b2c7d15 100644 --- a/tilia/utils.py +++ b/tilia/utils.py @@ -24,15 +24,15 @@ def open_with_os(path: Path) -> None: def load_dotenv() -> None: - dotenv_paths = [ - fn for fn in os.listdir(Path(__file__).parents[1]) if fn.endswith(".env") - ] + root = Path(__file__).parents[1] + dotenv_paths = [root / fn for fn in os.listdir(root) if fn.endswith(".env")] if dotenv_paths: import dotenv for p in dotenv_paths: - dotenv.load_dotenv(p) + with open(p) as f: + dotenv.load_dotenv(stream=f) else: # setup some basic values os.environ["LOG_REQUESTS"] = "1" @@ -41,6 +41,3 @@ def load_dotenv() -> None: ] = "TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE" if not os.environ.get("ENVIRONMENT"): os.environ["ENVIRONMENT"] = "dev" - - -load_dotenv() From a3032354e7e758cfa2fff19c2a1daba639b90f4f Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 9 Feb 2026 00:02:03 +0000 Subject: [PATCH 32/75] fix: loading dotenv first --- tests/conftest.py | 4 +--- tilia/boot.py | 5 +---- tilia/utils.py | 3 +++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cf8edeba1..7306fc316 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,7 @@ from PySide6.QtWidgets import QApplication from colorama import Fore, Style -from tilia.utils import load_dotenv - -load_dotenv() +import tilia.utils # noqa: F401 import tilia.constants as constants_module import tilia.log as logging_module diff --git a/tilia/boot.py b/tilia/boot.py index 5cf68ace9..9c70ae093 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -5,10 +5,7 @@ from PySide6.QtWidgets import QApplication -from tilia.utils import load_dotenv - -load_dotenv() - +import tilia.utils # noqa: F401 from tilia.app import App from tilia.clipboard import Clipboard from tilia.dirs import setup_dirs diff --git a/tilia/utils.py b/tilia/utils.py index 85b2c7d15..5b0766d1d 100644 --- a/tilia/utils.py +++ b/tilia/utils.py @@ -41,3 +41,6 @@ def load_dotenv() -> None: ] = "TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE" if not os.environ.get("ENVIRONMENT"): os.environ["ENVIRONMENT"] = "dev" + + +load_dotenv() From 4ed5b598d655a325154086d2e23abe9ecdbabfb1 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:54:36 +0000 Subject: [PATCH 33/75] chore: migrate to ruff --- .github/workflows/run-tests.yml | 4 ++-- .pre-commit-config.yaml | 14 ++++--------- pyproject.toml | 9 ++++++++- tests/test_app.py | 2 +- .../timelines/score/test_score_timeline_ui.py | 4 ++-- tilia/log.py | 2 +- tilia/media/player/qtplayer.py | 4 ++-- tilia/parsers/score/musicxml.py | 4 ++-- tilia/settings.py | 1 - tilia/timelines/base/component/base.py | 2 +- tilia/timelines/base/timeline.py | 4 ++-- tilia/timelines/harmony/components/harmony.py | 7 ++++--- tilia/ui/cli/generate_scripts.py | 8 ++++---- tilia/ui/cli/player.py | 1 - tilia/ui/cli/timelines/imp.py | 2 +- tilia/ui/timelines/beat/dialogs.py | 6 +++--- tilia/ui/timelines/collection/collection.py | 3 +-- tilia/ui/timelines/copy_paste.py | 2 +- .../timelines/hierarchy/key_press_manager.py | 20 +++++++++++-------- tilia/ui/timelines/hierarchy/timeline.py | 10 ++++++---- tilia/ui/windows/svg_viewer.py | 10 ++++------ 21 files changed, 61 insertions(+), 58 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 876666af3..ad64908e2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -67,9 +67,9 @@ jobs: run: | pip freeze - - name: Lint with flake8 + - name: Lint with ruff run: | - flake8 + ruff check continue-on-error: true - name: Test with pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd2837f4f..c70117428 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,8 @@ repos: - id: debug-statements - id: check-yaml - - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.0 hooks: - - id: flake8 - args: - [ - "--max-line-length=88", - "--per-file-ignores=__init__.py:F401,tilia/__main__.py:E402,tilia/boot.py:E402,tests/conftest.py:E402", - "--extend-ignore=E203,E501", - "--exclude=tilia/dev", - ] + - id: ruff-check + args: [ --fix ] diff --git a/pyproject.toml b/pyproject.toml index 0e2306482..87519f383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ build = [ ci-tests = [ "coverage>=7.0.0", "coverage-badge>=1.0.0", - "flake8>=7.0.0", + "ruff>=0.15.0", "pytest-cov>=6.0.0", ] dev = [ @@ -90,6 +90,13 @@ env = [ "QT_QPA_PLATFORM=offscreen", ] +[tool.ruff] +exclude = ["tilia/dev"] +line-length = 88 +lint.ignore = ["E203","E501"] +lint.per-file-ignores = {"__init__.py"=["F401"]} +target-version = "py310" + [tool.setuptools.packages.find] where = ["."] exclude = [ diff --git a/tests/test_app.py b/tests/test_app.py index b8dab9b21..8e4c858f8 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -645,7 +645,7 @@ def test_moving_files(self, tla, media, tilia, qtui, tmp_path): new_media = old_media.rename(new_folder / media) # open file at new folder - with (patch_file_dialog(True, [str(new_tla)])): + with patch_file_dialog(True, [str(new_tla)]): commands.execute("file.open") assert tilia.player.media_path == str(new_media) diff --git a/tests/ui/timelines/score/test_score_timeline_ui.py b/tests/ui/timelines/score/test_score_timeline_ui.py index 9351c7517..e3b477c2f 100644 --- a/tests/ui/timelines/score/test_score_timeline_ui.py +++ b/tests/ui/timelines/score/test_score_timeline_ui.py @@ -151,7 +151,7 @@ def test_missing_staff_deletes_timeline(qtui, tls, tilia_errors, tmp_path): "staff_index": 0, "color": None, "comments": "", - "" "display_accidental": False, + "display_accidental": False, "kind": "NOTE", "hash": "", }, @@ -235,7 +235,7 @@ def test_symbol_staff_collision(qtui, tmp_path): json.dumps(file_data_with_symbols), encoding="utf-8" ) - with (patch_file_dialog(True, [tmp_file_with_symbols])): + with patch_file_dialog(True, [tmp_file_with_symbols]): commands.execute("file.open") score = get(Get.TIMELINE_UI_BY_ATTR, "TIMELINE_KIND", TimelineKind.SCORE_TIMELINE) diff --git a/tilia/log.py b/tilia/log.py index 8f61bad41..b02c78589 100644 --- a/tilia/log.py +++ b/tilia/log.py @@ -41,7 +41,7 @@ def __init__(self): self._dump_count = count() def setup(self): - match (env := os.environ.get("ENVIRONMENT")): + match env := os.environ.get("ENVIRONMENT"): case "test": self.disabled = True self.setup_sentry(env) diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py index 611ff8b15..2ddef4eb9 100644 --- a/tilia/media/player/qtplayer.py +++ b/tilia/media/player/qtplayer.py @@ -19,7 +19,7 @@ def wait_for_signal(signal: SignalInstance, value): def signal_wrapper(func): timer = QTimer(singleShot=True, interval=100) loop = QEventLoop() - success = False + success = False # noqa: F841 def value_checker(signal_value): if signal_value == value: @@ -28,7 +28,7 @@ def value_checker(signal_value): loop.quit() def check_signal(*args, **kwargs): - global success # noqa: F824 + global success if not func(*args, **kwargs): return False signal.connect(value_checker) diff --git a/tilia/parsers/score/musicxml.py b/tilia/parsers/score/musicxml.py index 2769226a2..a78236734 100644 --- a/tilia/parsers/score/musicxml.py +++ b/tilia/parsers/score/musicxml.py @@ -148,11 +148,11 @@ def _parse_attributes(part: etree._Element, part_id: str): ): match prev_note.tag: case "backup": - if not prev_note.find("duration") is None: + if prev_note.find("duration") is not None: # grace notes have no duration cur_div -= int(prev_note.find("duration").text) case _: - if not prev_note.find("duration") is None: + if prev_note.find("duration") is not None: # grace notes have no duration cur_div += int(prev_note.find("duration").text) diff --git a/tilia/settings.py b/tilia/settings.py index 3d346bfc0..20361bc6f 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -6,7 +6,6 @@ class SettingsManager(QObject): - DEFAULT_SETTINGS = { "general": { "auto-scroll": ScrollType.OFF, diff --git a/tilia/timelines/base/component/base.py b/tilia/timelines/base/component/base.py index 6dc6d30b9..707121f83 100644 --- a/tilia/timelines/base/component/base.py +++ b/tilia/timelines/base/component/base.py @@ -83,7 +83,7 @@ def validate_creation(cls, *args, **kwargs) -> tuple[bool, str]: @staticmethod def compose_validators( - validators: list[Callable[[], tuple[bool, str]]] + validators: list[Callable[[], tuple[bool, str]]], ) -> tuple[bool, str]: """Calls validators in order and returns (False, reason) if any fails. Returns (True, '') if all succeed.""" for validator in validators: diff --git a/tilia/timelines/base/timeline.py b/tilia/timelines/base/timeline.py index d96f649b3..0dc010837 100644 --- a/tilia/timelines/base/timeline.py +++ b/tilia/timelines/base/timeline.py @@ -272,7 +272,7 @@ def _get_base_state(self) -> dict: string_to_hash += str(value) + "|" string_to_hash += str(value) + "|" - state["hash"] = hash_function(f'{state["kind"]}|{string_to_hash}') + state["hash"] = hash_function(f"{state['kind']}|{string_to_hash}") return state @@ -329,7 +329,7 @@ def associate_to_timeline(self, timeline: Timeline): @staticmethod def _compose_validators( - validators: list[Callable[[], tuple[bool, str]]] + validators: list[Callable[[], tuple[bool, str]]], ) -> tuple[bool, str]: """Calls validators in order and returns (False, reason) if any fails.""" for validator in validators: diff --git a/tilia/timelines/harmony/components/harmony.py b/tilia/timelines/harmony/components/harmony.py index 8b3de430a..f6ad9d102 100644 --- a/tilia/timelines/harmony/components/harmony.py +++ b/tilia/timelines/harmony/components/harmony.py @@ -159,9 +159,10 @@ def _replace_special_abbreviations(text): def _get_music21_object_from_text( text: str, key: str -) -> tuple[music21.harmony.ChordSymbol | music21.roman.RomanNumeral, str] | tuple[ - None, None -]: +) -> ( + tuple[music21.harmony.ChordSymbol | music21.roman.RomanNumeral, str] + | tuple[None, None] +): text, prefixed_accidental = _extract_prefixed_accidental(text) text = _format_postfix_accidental(text) text = _replace_special_abbreviations(text) diff --git a/tilia/ui/cli/generate_scripts.py b/tilia/ui/cli/generate_scripts.py index 04210bbee..f5c393ab1 100644 --- a/tilia/ui/cli/generate_scripts.py +++ b/tilia/ui/cli/generate_scripts.py @@ -136,12 +136,12 @@ def _get_script_for_folder( if not (media_data or "set_media_length.txt" in filenames): print( - f'{"No suitable media found in " + folder_name + ".":<100}{"Folder skipped.":>14}' + f"{'No suitable media found in ' + folder_name + '.':<100}{'Folder skipped.':>14}" ) return if len(media_data) > 1: print( - f'{"Multiple media files found in " + folder_name + ".":<100}{"Folder skipped.":>14}' + f"{'Multiple media files found in ' + folder_name + '.':<100}{'Folder skipped.':>14}" ) return @@ -151,7 +151,7 @@ def _get_script_for_folder( if "set_media_length.txt" in filenames: to_write.append( - f'metadata set-media-length {open(os.path.join(folder_name, "set_media_length.txt"), "r").read()}\n' + f"metadata set-media-length {open(os.path.join(folder_name, 'set_media_length.txt'), 'r').read()}\n" ) filenames.remove("set_media_length.txt") @@ -173,7 +173,7 @@ def _get_script_for_folder( reference_beat = args.ref_name elif reference_beat and args.ref_name: print( - f'{"Multiple beat timelines found. Using " + reference_beat + " as reference instead of " + args.ref_name + ".":<100}{os.path.join(folder_name, file):>20}' + f"{'Multiple beat timelines found. Using ' + reference_beat + ' as reference instead of ' + args.ref_name + '.':<100}{os.path.join(folder_name, file):>20}" ) except ValueError as e: diff --git a/tilia/ui/cli/player.py b/tilia/ui/cli/player.py index fc5e9fa1e..1b8a86730 100644 --- a/tilia/ui/cli/player.py +++ b/tilia/ui/cli/player.py @@ -162,7 +162,6 @@ def get_youtube_duration(self, id: str) -> tuple[bool, str | float]: return False, self.DECODE_ERROR_MESSAGE try: - duration = response_json["items"][0]["contentDetails"]["duration"] except IndexError: return False, self.VIDEO_NOT_FOUND_MESSAGE.format(id) diff --git a/tilia/ui/cli/timelines/imp.py b/tilia/ui/cli/timelines/imp.py index 8ca8e1fb5..f9ce5a5d0 100644 --- a/tilia/ui/cli/timelines/imp.py +++ b/tilia/ui/cli/timelines/imp.py @@ -14,7 +14,7 @@ def setup_parser(subparsers): # Import command import_parser = subparsers.add_parser( - "import", help="Import data from a file into a " "timeline" + "import", help="Import data from a file into a timeline" ) import_parser.set_defaults(func=import_timeline) diff --git a/tilia/ui/timelines/beat/dialogs.py b/tilia/ui/timelines/beat/dialogs.py index 06c9a422f..c6a28192e 100644 --- a/tilia/ui/timelines/beat/dialogs.py +++ b/tilia/ui/timelines/beat/dialogs.py @@ -21,7 +21,7 @@ def validate_result(): return True, list(map(int, result)) -def ask_beat_timeline_fill_method() -> ( - tuple[bool, None | tuple[BeatTimeline, BeatTimeline.FillMethod, float]] -): +def ask_beat_timeline_fill_method() -> tuple[ + bool, None | tuple[BeatTimeline, BeatTimeline.FillMethod, float] +]: return FillBeatTimeline.select() diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index 5c5ea46f0..c454c71f9 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -704,8 +704,7 @@ def _on_timeline_ui_right_click( if not timeline_ui: raise ValueError( - f"Can't process left click: no timeline with view '{view}' on" - f" {self}" + f"Can't process left click: no timeline with view '{view}' on {self}" ) if ( diff --git a/tilia/ui/timelines/copy_paste.py b/tilia/ui/timelines/copy_paste.py index 48fc104bc..5dcc8ac98 100644 --- a/tilia/ui/timelines/copy_paste.py +++ b/tilia/ui/timelines/copy_paste.py @@ -8,7 +8,7 @@ def get_copy_data_from_elements( - elements: list[tuple[TimelineUIElement, CopyAttributes]] + elements: list[tuple[TimelineUIElement, CopyAttributes]], ) -> list[dict]: copy_data = [] for element, kind, copy_attrs in elements: diff --git a/tilia/ui/timelines/hierarchy/key_press_manager.py b/tilia/ui/timelines/hierarchy/key_press_manager.py index f0f7a34b1..30e7c544d 100644 --- a/tilia/ui/timelines/hierarchy/key_press_manager.py +++ b/tilia/ui/timelines/hierarchy/key_press_manager.py @@ -41,10 +41,12 @@ def on_vertical_arrow_press(self, direction: str): def on_horizontal_arrow_press(self, side: str): def _get_next_element_in_same_level(elm): - is_later_at_same_level = ( - lambda h: h.tl_component.start > elm.tl_component.start - and h.tl_component.level == elm.tl_component.level - ) + def is_later_at_same_level(h): + return ( + h.tl_component.start > elm.tl_component.start + and h.tl_component.level == elm.tl_component.level + ) + later_elements = self.element_manager.get_elements_by_condition( is_later_at_same_level ) @@ -54,10 +56,12 @@ def _get_next_element_in_same_level(elm): return None def _get_previous_element_in_same_level(elm): - is_earlier_at_same_level = ( - lambda h: h.tl_component.start < elm.tl_component.start - and h.tl_component.level == elm.tl_component.level - ) + def is_earlier_at_same_level(h): + return ( + h.tl_component.start < elm.tl_component.start + and h.tl_component.level == elm.tl_component.level + ) + earlier_elements = self.element_manager.get_elements_by_condition( is_earlier_at_same_level ) diff --git a/tilia/ui/timelines/hierarchy/timeline.py b/tilia/ui/timelines/hierarchy/timeline.py index 7a9210b88..2cad39416 100644 --- a/tilia/ui/timelines/hierarchy/timeline.py +++ b/tilia/ui/timelines/hierarchy/timeline.py @@ -230,10 +230,12 @@ def paste_with_children_into_elements( self, elements: list[HierarchyUI], data: list[dict] ): def get_descendants(parent: HierarchyUI): - is_in_branch = ( - lambda e: e.tl_component.start >= parent.tl_component.start - and e.tl_component.end <= parent.tl_component.end - ) + def is_in_branch(e): + return ( + e.tl_component.start >= parent.tl_component.start + and e.tl_component.end <= parent.tl_component.end + ) + elements_in_branch = self.element_manager.get_elements_by_condition( is_in_branch ) diff --git a/tilia/ui/windows/svg_viewer.py b/tilia/ui/windows/svg_viewer.py index db247938f..594a673c3 100644 --- a/tilia/ui/windows/svg_viewer.py +++ b/tilia/ui/windows/svg_viewer.py @@ -408,9 +408,8 @@ def annotation_font_inc(self) -> None: def _get_time_from_scene_x(self, xs: dict[int, float]) -> dict[int, list[float]]: output = {} beat_pos = {} - beats, x_pos = list(self.beat_x_position.keys()), list( - self.beat_x_position.values() - ) + beats = list(self.beat_x_position.keys()) + x_pos = list(self.beat_x_position.values()) for key, x in xs.items(): x = round(x, 3) if x in x_pos: @@ -476,9 +475,8 @@ def _get_scene_x_from_time(self, time: float) -> float: beat = beat_tl.get_metric_fraction_by_time(time) if x := self.beat_x_position.get(beat): return x - beats, x_pos = list(self.beat_x_position.keys()), list( - self.beat_x_position.values() - ) + beats = list(self.beat_x_position.keys()) + x_pos = list(self.beat_x_position.values()) idx = bisect(beats, beat) if idx == 0: return x_pos[0] From d5c9724e1e0ef8321a7c23b5c5b17ba1cf4d7795 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:46:37 +0000 Subject: [PATCH 34/75] ci: fix mac build tests --- .github/workflows/build.yml | 6 ++++-- scripts/deploy.py | 29 ++++++++++++++++++----------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08f151f12..e25ac6d90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,6 @@ jobs: run: | brew remove --force --ignore-dependencies openssl@3 brew cleanup openssl@3 - brew install coreutils - name: Install dependencies run: | @@ -64,8 +63,11 @@ jobs: if: runner.os == 'macOS' shell: bash run: | + brew install coreutils echo "Checking dependencies" - echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true + zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true + rm -rf ${{ steps.build-exe.outputs.out-filepath }} - name: Test executable [Windows] if: runner.os == 'Windows' diff --git a/scripts/deploy.py b/scripts/deploy.py index 152a3c0b8..5af75a236 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -50,13 +50,16 @@ def _handle_inputs(): assert len(sys.argv) == 3, "Incorrect number of inputs" global ref_name, build_os, outdir ref_name = sys.argv[1] - build_os = "-".join( - [ - x - for x in sys.argv[2].split("-") - if not x.replace(".", "", 1).isdigit() and x != "latest" - ] - ) + if "macos" in sys.argv[2] and "intel" not in sys.argv[2]: + build_os = "macos-silicon" + else: + build_os = "-".join( + [ + x + for x in sys.argv[2].split("-") + if not x.replace(".", "", 1).isdigit() and x != "latest" + ] + ) outdir = buildlib / build_os @@ -91,7 +94,6 @@ def _get_exe_cmd() -> list[str]: f"--product-name={name}", f"--file-version={version}", f"--output-filename={out_filename}", - # "--onefile-tempdir-spec={CACHE_DIR}/{PRODUCT}/{VERSION}", f"--macos-app-icon={icon_path}", "--macos-app-mode=gui", f"--macos-app-version={version}", @@ -242,11 +244,16 @@ def build(): _build_exe() if os.environ.get("GITHUB_OUTPUT"): if "mac" in build_os: - global outdir - outdir = outdir / "tilia.app" / "Contents" / "MacOS" + out_filepath = outdir / "exe" / "tilia.app" + else: + out_filepath = outdir / "exe" / out_filename with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write(f"out-filepath={(outdir / 'exe' / out_filename).as_posix()}\n") + f.write(f"out-filepath={out_filepath.as_posix()}\n") f.write(f"out-filename={out_filename}\n") + if "mac" in build_os: + f.write( + f"zip-filepath={outdir.as_posix()}/exe/{out_filename}.zip\n" + ) os.chdir(old_dir) dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var) except Exception as e: From 3b2258e8e8d8a5396c548d3bed143c649749d920 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:30:20 +0000 Subject: [PATCH 35/75] refactor: mostly typing fixes --- tests/conftest.py | 2 +- tests/mock.py | 8 +++--- tests/parsers/csv/test_harmony_from_csv.py | 2 +- tests/player/test_player.py | 2 +- tests/test_app.py | 6 ++--- tests/ui/cli/test_load_media.py | 4 ++- tests/ui/dialogs/test_crash.py | 4 +-- tests/ui/timelines/harmony/test_harmony_ui.py | 4 +-- tests/ui/timelines/harmony/test_mode_ui.py | 4 +-- .../timelines/hierarchy/test_hierarchy_ui.py | 2 +- tests/ui/timelines/interact.py | 4 +-- tests/ui/timelines/score/test_musicxml.py | 2 +- .../timelines/test_timelineui_collection.py | 2 +- tests/ui/windows/test_manage_timelines.py | 4 +-- tilia/app.py | 2 +- tilia/constants.py | 4 +-- tilia/parsers/csv/beat.py | 2 +- tilia/parsers/csv/common.py | 4 +-- tilia/parsers/csv/hierarchy.py | 4 +-- tilia/parsers/csv/marker.py | 4 +-- tilia/parsers/score/timewise_to_partwise.xsl | 2 +- tilia/requests/get.py | 10 +++---- tilia/requests/post.py | 2 +- tilia/settings.py | 6 ++--- tilia/timelines/base/component/base.py | 11 ++++---- tilia/timelines/base/timeline.py | 8 +++--- tilia/timelines/beat/timeline.py | 2 +- tilia/timelines/collection/collection.py | 8 +++--- tilia/timelines/harmony/components/harmony.py | 6 +++-- tilia/timelines/hierarchy/timeline.py | 2 +- tilia/timelines/slider/timeline.py | 2 +- tilia/ui/cli/generate_scripts.py | 2 +- tilia/ui/cli/timelines/list.py | 2 +- tilia/ui/cli/ui.py | 6 ++++- tilia/ui/qtui.py | 4 +-- tilia/ui/timelines/base/element.py | 8 +++++- tilia/ui/timelines/base/element_manager.py | 8 +++--- tilia/ui/timelines/base/timeline.py | 2 +- tilia/ui/timelines/beat/element.py | 13 +++++----- tilia/ui/timelines/beat/timeline.py | 2 +- tilia/ui/timelines/collection/collection.py | 14 +++++----- tilia/ui/timelines/copy_paste.py | 2 +- tilia/ui/timelines/hierarchy/element.py | 26 +++++++++---------- tilia/ui/timelines/pdf/timeline.py | 2 +- tilia/ui/timelines/score/element/barline.py | 11 ++++++-- tilia/ui/timelines/score/element/clef.py | 6 +++++ .../timelines/score/element/key_signature.py | 8 +++++- tilia/ui/timelines/score/element/note/ui.py | 14 +++++----- tilia/ui/timelines/score/element/staff.py | 6 +++++ .../timelines/score/element/time_signature.py | 15 ++++++++--- tilia/ui/timelines/score/timeline.py | 4 +-- tilia/ui/windows/settings.py | 18 +++++++++---- 52 files changed, 177 insertions(+), 125 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7306fc316..b145a8503 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,7 +196,7 @@ def resources() -> Path: @pytest.fixture(scope="module") def use_test_settings(qapplication): settings_module.settings._settings = QSettings( - constants_module.APP_NAME, "DesktopTests" + constants_module.APP_NAME, "DesktopTests", None ) settings_module.settings._check_all_default_settings_present() settings_module.settings.set("general", "prioritise_performance", True) diff --git a/tests/mock.py b/tests/mock.py index 54c1529f9..a884c1cba 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -51,8 +51,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _callback(self, *_, **__): try: return_value = self.return_values[self.return_count] - except IndexError: - raise IndexError("Not enough return values to serve") + except IndexError as e: + raise IndexError("Not enough return values to serve") from e self.return_count += 1 return return_value @@ -142,6 +142,6 @@ def patch_ask_for_string_dialog(success: bool | list[bool], string: str | list[s else: raise ValueError("input must be a string if success is a bool.") - return_values = list(zip(string, success)) - with (patch.object(QInputDialog, "getText", side_effect=return_values),): + return_values = list(zip(string, success, strict=True)) + with patch.object(QInputDialog, "getText", side_effect=return_values): yield diff --git a/tests/parsers/csv/test_harmony_from_csv.py b/tests/parsers/csv/test_harmony_from_csv.py index efe814ddc..e090779e8 100644 --- a/tests/parsers/csv/test_harmony_from_csv.py +++ b/tests/parsers/csv/test_harmony_from_csv.py @@ -82,7 +82,7 @@ def test_fails_without_a_required_column(self, required_attr, harmony_tl): success, errors = call_patched_import_by_time_func(harmony_tl, data) assert_in_errors(required_attr, errors) - def test_returns_error_for_invalid_rows_and_processess_valid_rows(self, harmony_tl): + def test_returns_error_for_invalid_rows_and_processes_valid_rows(self, harmony_tl): data = "\n".join( [ "time,harmony_or_key,symbol", diff --git a/tests/player/test_player.py b/tests/player/test_player.py index 1e16c61fd..5c2e39e13 100644 --- a/tests/player/test_player.py +++ b/tests/player/test_player.py @@ -11,7 +11,7 @@ def conservative_player_stop(tilia): """ Increases player.SLEEP_AFTER_STOP to 5 seconds if on CI. Avoids freezes when setting URL after stop. Workaround for running tests on CI. - Proper hadling of player status changes would be a more robust solution. + Proper handling of player status changes would be a more robust solution. """ original_sleep_after_stop = tilia.player.SLEEP_AFTER_STOP diff --git a/tests/test_app.py b/tests/test_app.py index 8e4c858f8..a1be29f61 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -28,7 +28,7 @@ class TestLogger: def test_sentry_not_logging(self): # TODO: make this test run first in batch testing. - # enabling sentry during tests will fill inbox up unneccesarily + # enabling sentry during tests will fill inbox up unnecessarily assert "tilia.log" in tilia.log.sentry_sdk.integrations.logging._IGNORED_LOGGERS @@ -493,7 +493,7 @@ def test_open_without_saving_changes(self, tilia, tls, marker_tlui, tmp_path): assert len(tls) == 2 # assert load was successful assert len(list(contents["timelines"][str(prev_tl_id)]["components"])) == 0 - def test_open_canceling_should_save_changes_dialog( + def test_open_cancelling_should_save_changes_dialog( self, tilia, tls, marker_tlui, tmp_path ): previous_path = tmp_path / "previous.tla" @@ -584,7 +584,7 @@ def test_all_windows_are_closed(self, tilia, qtui): with Serve(Get.FROM_USER_SHOULD_SAVE_CHANGES, (True, False)): commands.execute("file.new") - # this doesn't actaully check if windows are closed + # this doesn't actually check if windows are closed # it checks if app._windows[kind] is None. # Those should be equivalent, if everything is working as it should assert not any(qtui.is_window_open(k) for k in WindowKind) diff --git a/tests/ui/cli/test_load_media.py b/tests/ui/cli/test_load_media.py index ca1cb9b3d..949cb8912 100644 --- a/tests/ui/cli/test_load_media.py +++ b/tests/ui/cli/test_load_media.py @@ -92,7 +92,9 @@ def test_with_timelines_scale_not_provided_answer_yes_but_dont_confirm_crop( assert marker_tl[0].get_data("time") == 50 * EXAMPLE_MEDIA_SCALE_FACTOR -def test_with_timelines_scale_not_provied_answer_crop(cli, tilia_state, marker_tl): +def test_with_timelines_scale_not_provided_answer_crop( + cli, tilia_state, marker_tl +): for time in [5, 50]: marker_tl.create_marker(time) diff --git a/tests/ui/dialogs/test_crash.py b/tests/ui/dialogs/test_crash.py index e75c680a5..f1261247d 100644 --- a/tests/ui/dialogs/test_crash.py +++ b/tests/ui/dialogs/test_crash.py @@ -40,7 +40,7 @@ def test_crash_support_empty_fields(self): set_user.assert_not_called() - def test_crash_support_remmeber(self): + def test_crash_support_remember(self): crash_support_dialog = CrashSupportDialog(None) crash_support_dialog.email_field.setText("an invalid email") crash_support_dialog.name_field.setText("John Doe") @@ -51,7 +51,7 @@ def test_crash_support_remmeber(self): set_user.assert_called_once_with("", "John Doe") - def test_crash_support_not_remmeber(self): + def test_crash_support_not_remember(self): crash_support_dialog = CrashSupportDialog(None) crash_support_dialog.email_field.setText("an invalid email") crash_support_dialog.name_field.setText("John Doe") diff --git a/tests/ui/timelines/harmony/test_harmony_ui.py b/tests/ui/timelines/harmony/test_harmony_ui.py index bb371e70b..a1861db81 100644 --- a/tests/ui/timelines/harmony/test_harmony_ui.py +++ b/tests/ui/timelines/harmony/test_harmony_ui.py @@ -79,7 +79,7 @@ def test_paste_single_into_element(self, tlui): click_harmony_ui(tlui[1]) commands.execute("timeline.component.paste") assert len(tlui) == 2 - for attr, value in attributes_to_copy.items(): + for attr in attributes_to_copy.keys(): assert tlui[1].get_data(attr) == attributes_to_copy[attr] def test_paste_multiple_into_element(self, tlui): @@ -117,5 +117,5 @@ def test_paste_multiple_into_element(self, tlui): click_harmony_ui(target_hui) commands.execute("timeline.component.paste") assert len(tlui) == 6 - for attr, value in attributes_to_copy.items(): + for attr in attributes_to_copy.keys(): assert target_hui.get_data(attr) == attributes_to_copy[attr] diff --git a/tests/ui/timelines/harmony/test_mode_ui.py b/tests/ui/timelines/harmony/test_mode_ui.py index 0fbadcd51..651898711 100644 --- a/tests/ui/timelines/harmony/test_mode_ui.py +++ b/tests/ui/timelines/harmony/test_mode_ui.py @@ -71,7 +71,7 @@ def test_paste_single_into_element(self, tlui): click_mode_ui(tlui[1]) commands.execute("timeline.component.paste") assert len(tlui) == 2 - for attr, value in attributes_to_copy.items(): + for attr in attributes_to_copy.keys(): assert tlui[1].get_data(attr) == attributes_to_copy[attr] def test_paste_multiple_into_element(self, tlui): @@ -101,5 +101,5 @@ def test_paste_multiple_into_element(self, tlui): click_mode_ui(target_mui) commands.execute("timeline.component.paste") assert len(tlui) == 6 - for attr, value in attributes_to_copy.items(): + for attr in attributes_to_copy.keys(): assert target_mui.get_data(attr) == attributes_to_copy[attr] diff --git a/tests/ui/timelines/hierarchy/test_hierarchy_ui.py b/tests/ui/timelines/hierarchy/test_hierarchy_ui.py index 2c3f29e71..57c16e2ab 100644 --- a/tests/ui/timelines/hierarchy/test_hierarchy_ui.py +++ b/tests/ui/timelines/hierarchy/test_hierarchy_ui.py @@ -30,7 +30,7 @@ def test_full_name_no_label(self, tlui): assert ( tlui[0].full_name - == "tl" + HierarchyUI.FULL_NAME_SEPARATOR + HierarchyUI.NAME_WHEN_UNLABELED + == "tl" + HierarchyUI.FULL_NAME_SEPARATOR + HierarchyUI.NAME_WHEN_UNLABELLED ) def test_full_name_with_parent(self, tlui): diff --git a/tests/ui/timelines/interact.py b/tests/ui/timelines/interact.py index 0c4e72095..7e7ee6724 100644 --- a/tests/ui/timelines/interact.py +++ b/tests/ui/timelines/interact.py @@ -92,8 +92,8 @@ def press_key(key: str, modifier: Qt.KeyboardModifier | None = None): if len(key) > 1: try: key = getattr(Qt.Key, f"Key_{key}") - except AttributeError: - raise ValueError(f"Unknown key: {key}") + except AttributeError as e: + raise ValueError(f"Unknown key: {key}") from e QTest.keyClick( get_focused_widget(), key, modifier=modifier or Qt.KeyboardModifier.NoModifier diff --git a/tests/ui/timelines/score/test_musicxml.py b/tests/ui/timelines/score/test_musicxml.py index 1fefb500f..39cddace4 100644 --- a/tests/ui/timelines/score/test_musicxml.py +++ b/tests/ui/timelines/score/test_musicxml.py @@ -13,7 +13,7 @@ def setup_valid_beats(self, beat_tl): beat_tl.recalculate_measures() - def test_user_accpets(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state): + def test_user_accepts(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state): self.setup_valid_beats(beat_tl) with patch_yes_or_no_dialog(True): diff --git a/tests/ui/timelines/test_timelineui_collection.py b/tests/ui/timelines/test_timelineui_collection.py index 9896ed1f2..8edb37889 100644 --- a/tests/ui/timelines/test_timelineui_collection.py +++ b/tests/ui/timelines/test_timelineui_collection.py @@ -222,7 +222,7 @@ def test_by_page_is_triggered(self, tluis): move_to_x_mock.assert_called() center_on_time_mock.assert_not_called() - def test_by_page_is_not_triggered_when_not_over_treshold(self, tluis): + def test_by_page_is_not_triggered_when_not_over_threshold(self, tluis): self._set_auto_scroll(ScrollType.BY_PAGE) with patch.object(tluis.view, "move_to_x") as move_to_x_mock: post(Post.PLAYER_CURRENT_TIME_CHANGED, 10, MediaTimeChangeReason.PLAYBACK) diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py index dbba21621..e0cec94b1 100644 --- a/tests/ui/windows/test_manage_timelines.py +++ b/tests/ui/windows/test_manage_timelines.py @@ -14,8 +14,8 @@ def assert_order_is_correct(tls: Timelines, expected: list[Timeline]): # assert timeline order - for tl, expected in zip(sorted(tls), expected): - assert tl == expected + for tl, e in zip(sorted(tls), expected, strict=True): + assert tl == e # assert list widget order for i, tl in enumerate(expected): diff --git a/tilia/app.py b/tilia/app.py index fd8c5d970..2792234e4 100644 --- a/tilia/app.py +++ b/tilia/app.py @@ -169,7 +169,7 @@ def on_open(self, path: Path | str | None = None) -> None: def update_recent_files(self): try: geometry, window_state = get(Get.WINDOW_GEOMETRY), get(Get.WINDOW_STATE) - except tilia.exceptions.NoReplyToRequest: + except NoReplyToRequest: geometry, window_state = None, None settings.update_recent_files( diff --git a/tilia/constants.py b/tilia/constants.py index 34b6ba52e..6468db56d 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -3,9 +3,9 @@ import re if (toml := Path(__file__).parent.parent / "pyproject.toml").exists(): - from sys import version_info + import sys - if version_info >= (3, 11): + if sys.version_info >= (3, 11): from tomllib import load else: from tomli import load diff --git a/tilia/parsers/csv/beat.py b/tilia/parsers/csv/beat.py index 594c70974..3eb2bd821 100644 --- a/tilia/parsers/csv/beat.py +++ b/tilia/parsers/csv/beat.py @@ -55,7 +55,7 @@ def beats_from_csv( measures_to_force_display = [] def check_params() -> bool: - for param, parser in zip(params, parsers): + for param, parser in zip(params, parsers, strict=True): if param in params_to_indices: try: index = params_to_indices[param] diff --git a/tilia/parsers/csv/common.py b/tilia/parsers/csv/common.py index f04ff9528..08ee6ba6d 100644 --- a/tilia/parsers/csv/common.py +++ b/tilia/parsers/csv/common.py @@ -73,8 +73,8 @@ def _get_attr_data( def _parse_measure_fraction(value: str): try: value = float(value) - except ValueError: - raise ValueError("APPEND:Must be a number between 0 and 1.") + except ValueError as e: + raise ValueError("APPEND:Must be a number between 0 and 1.") from e if not 0 <= value <= 1: raise ValueError("APPEND:Must be a number between 0 and 1.") diff --git a/tilia/parsers/csv/hierarchy.py b/tilia/parsers/csv/hierarchy.py index e3ee5a431..ea5632dca 100644 --- a/tilia/parsers/csv/hierarchy.py +++ b/tilia/parsers/csv/hierarchy.py @@ -41,7 +41,7 @@ def import_by_time( "formal_type", "formal_function", ] - parsers = [float, float, int, float, float, str, str, str] + parsers = [float, float, int, float, float, str, str, str, str, str] params_to_indices = get_params_indices(params, next(reader)) for attr in ["start", "end", "level"]: @@ -61,7 +61,7 @@ def import_by_time( errors.append(f"'{value}' is not a valid {attr.replace('_', ' ')}") continue - for param, parser in zip(params, parsers): + for param, parser in zip(params, parsers, strict=True): if param in params_to_indices: index = params_to_indices[param] constructor_args[param] = parser(row[index]) diff --git a/tilia/parsers/csv/marker.py b/tilia/parsers/csv/marker.py index 95ec0e0ac..1a96e3a1b 100644 --- a/tilia/parsers/csv/marker.py +++ b/tilia/parsers/csv/marker.py @@ -50,7 +50,7 @@ def import_by_time( parsers = [float, str, str] constructor_kwargs = {} - for param, parser in zip(params, parsers): + for param, parser in zip(params, parsers, strict=True): if param in params_to_indices: index = params_to_indices[param] constructor_kwargs[param] = parser(row[index]) @@ -124,7 +124,7 @@ def import_by_measure( parsers = [str, str] constructor_kwargs = {} - for param, parser in zip(params, parsers): + for param, parser in zip(params, parsers, strict=True): if param in params_to_indices: index = params_to_indices[param] constructor_kwargs[param] = parser(row[index]) diff --git a/tilia/parsers/score/timewise_to_partwise.xsl b/tilia/parsers/score/timewise_to_partwise.xsl index f0e0925e4..b61adf97f 100644 --- a/tilia/parsers/score/timewise_to_partwise.xsl +++ b/tilia/parsers/score/timewise_to_partwise.xsl @@ -52,7 +52,7 @@ diff --git a/tilia/requests/get.py b/tilia/requests/get.py index ce3bc1e30..6d3fb9eb5 100644 --- a/tilia/requests/get.py +++ b/tilia/requests/get.py @@ -80,8 +80,8 @@ def get(request: Get, *args, **kwargs) -> Any: try: return _requests_to_callbacks[request](*args, **kwargs) - except KeyError: - raise NoReplyToRequest(f"{request} has no repliers attached.") + except KeyError as e: + raise NoReplyToRequest(f"{request} has no repliers attached.") from e except Exception as exc: raise Exception( f"Exception when processing {request} with {args=}, {kwargs=}" @@ -110,12 +110,12 @@ def server(request: Get) -> tuple[Any | None, Callable | None]: def stop_serving(replier: Any, request: Get) -> None: """ - Detaches a calback from a request. + Detaches a callback from a request. """ try: _requests_to_callbacks.pop(request) - except KeyError: - raise NoCallbackAttached() + except KeyError as e: + raise NoCallbackAttached() from e _servers_to_requests[replier].remove(request) if not _servers_to_requests[replier]: diff --git a/tilia/requests/post.py b/tilia/requests/post.py index d0ace8aa4..9f78aba5f 100644 --- a/tilia/requests/post.py +++ b/tilia/requests/post.py @@ -130,7 +130,7 @@ def post(post: Post, *args, **kwargs) -> None: # Should be used only when a single listener is expected. # If there are multiple listeners, the result of the last listener is returned. result = None - for listener, callback in _posts_to_listeners[post].copy().items(): + for callback in _posts_to_listeners[post].copy().values(): result = callback(*args, **kwargs) return result diff --git a/tilia/settings.py b/tilia/settings.py index 20361bc6f..373dc40c4 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -89,7 +89,7 @@ class SettingsManager(QObject): } def __init__(self): - self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings") + self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings", None) self._files_updated_callbacks = set() self._cache = {} self._check_all_default_settings_present() @@ -150,8 +150,8 @@ def set(self, group_name: str, setting: str, value): try: self._cache[group_name][setting] = value self._set(group_name, setting, value) - except AttributeError: - raise AttributeError(f"{group_name}.{setting} not found in cache.") + except AttributeError as e: + raise AttributeError(f"{group_name}.{setting} not found in cache.") from e @staticmethod def _get_key(group_name: str, setting: str, in_default: bool) -> str: diff --git a/tilia/timelines/base/component/base.py b/tilia/timelines/base/component/base.py index 707121f83..6e1b8e4f2 100644 --- a/tilia/timelines/base/component/base.py +++ b/tilia/timelines/base/component/base.py @@ -1,6 +1,5 @@ from __future__ import annotations -from abc import ABC from typing import Any, Callable from tilia.exceptions import SetComponentDataError, GetComponentDataError @@ -10,7 +9,7 @@ from tilia.utils import get_tilia_class_string -class TimelineComponent(ABC): +class TimelineComponent: SERIALIZABLE = [] ORDERING_ATTRS = tuple() @@ -54,10 +53,10 @@ def validate_set_data(self, attr, value): ) try: return self.validators[attr](value) - except KeyError: + except KeyError as e: raise KeyError( f"{self} has no validator for attribute {attr}. Can't set to '{value}'." - ) + ) from e def set_data(self, attr: str, value: Any): if not self.validate_set_data(attr, value): @@ -71,11 +70,11 @@ def set_data(self, attr: str, value: Any): def get_data(self, attr: str): try: return getattr(self, attr) - except AttributeError: + except AttributeError as e: raise GetComponentDataError( "AttributeError while getting data from component." f"Does {type(self)} have a {attr} attribute?" - ) + ) from e @classmethod def validate_creation(cls, *args, **kwargs) -> tuple[bool, str]: diff --git a/tilia/timelines/base/timeline.py b/tilia/timelines/base/timeline.py index 0dc010837..2d4b65a83 100644 --- a/tilia/timelines/base/timeline.py +++ b/tilia/timelines/base/timeline.py @@ -164,10 +164,10 @@ def validate_set_data(self, attr, value): ) try: return self.validators[attr](value) - except KeyError: + except KeyError as e: raise KeyError( f"{self} has no validator for attribute {attr}. Can't set to '{value}'." - ) + ) from e def set_data(self, attr: str, value: Any): if not self.validate_set_data(attr, value): @@ -470,11 +470,11 @@ def _remove_from_components_set(self, component: TC) -> None: try: self._components.remove(component) self.id_to_component.pop(component.id) - except KeyError: + except KeyError as e: raise KeyError( f"Can't remove component '{component}' from {self}: not in" " self.components." - ) + ) from e def update_component_order(self, component: TC): self._components.remove(component) diff --git a/tilia/timelines/beat/timeline.py b/tilia/timelines/beat/timeline.py index a954058b9..fa13f3f6b 100644 --- a/tilia/timelines/beat/timeline.py +++ b/tilia/timelines/beat/timeline.py @@ -577,7 +577,7 @@ def get_beat_index(self, beat: Beat) -> int: return self.components.index(beat) def propagate_measure_number_change(self, start_index: int): - for j, measure in enumerate(self.measure_numbers[start_index + 1 :]): + for j in range(len(self.measure_numbers[start_index + 1 :])): propagate_index = j + start_index + 1 if propagate_index in self.measures_to_force_display: break diff --git a/tilia/timelines/collection/collection.py b/tilia/timelines/collection/collection.py index 36cb62cf1..962f13dc3 100644 --- a/tilia/timelines/collection/collection.py +++ b/tilia/timelines/collection/collection.py @@ -86,10 +86,10 @@ def _validate_timeline_kind(kind: TlKind | str): if isinstance(kind, str): try: kind = TlKind(kind) - except ValueError: + except ValueError as e: raise TimelineValidationError( f"Can't create timeline: invalid timeline kind '{kind}'" - ) + ) from e if not isinstance(kind, TlKind): raise TimelineValidationError( f"Can't create timeline: invalid timeline kind '{kind}'" @@ -204,11 +204,11 @@ def _remove_from_timelines(self, timeline: Timeline) -> None: for tl in self: if tl.ordinal > timeline.ordinal: tl.ordinal -= 1 - except ValueError: + except ValueError as e: raise ValueError( f"Can't remove timeline '{timeline}' from {self}: not in" " self._timelines." - ) + ) from e def get_export_data(self): return [ diff --git a/tilia/timelines/harmony/components/harmony.py b/tilia/timelines/harmony/components/harmony.py index f6ad9d102..182925e39 100644 --- a/tilia/timelines/harmony/components/harmony.py +++ b/tilia/timelines/harmony/components/harmony.py @@ -103,9 +103,11 @@ def __repr__(self): @classmethod def from_string( - cls, time: float, string: str, key: music21.key.Key = music21.key.Key("C") + cls, time: float, string: str, key: music21.key.Key | str = "C major" ): - music21_object, object_type = _get_music21_object_from_text(string, key) + music21_object, object_type = _get_music21_object_from_text( + string, key.__str__() + ) if not string: return None diff --git a/tilia/timelines/hierarchy/timeline.py b/tilia/timelines/hierarchy/timeline.py index 68797c1b9..d4422f16d 100644 --- a/tilia/timelines/hierarchy/timeline.py +++ b/tilia/timelines/hierarchy/timeline.py @@ -95,7 +95,7 @@ def get_boundary_conflicts(self) -> list[tuple[Hierarchy, Hierarchy]]: and hrc1.end == hrc2.end and hrc1.level == hrc2.level ): - # if hierachies have same times and level, there's a conflict + # if hierarchies have same times and level, there's a conflict conflicts.append((hrc1, hrc2)) return conflicts diff --git a/tilia/timelines/slider/timeline.py b/tilia/timelines/slider/timeline.py index 49fd61560..02c5a296e 100644 --- a/tilia/timelines/slider/timeline.py +++ b/tilia/timelines/slider/timeline.py @@ -32,7 +32,7 @@ def is_empty(self): return True def _validate_delete_components(self, component: TimelineComponent): - """Nothing to do. Must impement abstract method.""" + """Nothing to do. Must implement abstract method.""" def get_state(self) -> dict: result = {} diff --git a/tilia/ui/cli/generate_scripts.py b/tilia/ui/cli/generate_scripts.py index f5c393ab1..6e5b5f3c7 100644 --- a/tilia/ui/cli/generate_scripts.py +++ b/tilia/ui/cli/generate_scripts.py @@ -218,7 +218,7 @@ def get_scripts(directory: str | Path) -> list[Path]: List of paths to individual scripts (str): The absolute path to each script. """ saved_scripts = [] - for folder_name, sub_folders, filenames in os.walk(directory): + for folder_name, _, filenames in os.walk(directory): saved_scripts += filter(None, [_get_script_for_folder(folder_name, filenames)]) return saved_scripts diff --git a/tilia/ui/cli/timelines/list.py b/tilia/ui/cli/timelines/list.py index dc8d7ba8c..1f140e417 100644 --- a/tilia/ui/cli/timelines/list.py +++ b/tilia/ui/cli/timelines/list.py @@ -4,7 +4,7 @@ def pprint_tlkind(kind: TlKind) -> str: - return kind.value.strip("_TIMELINE").capitalize() + return kind.value.replace("_TIMELINE", "").capitalize() def setup_parser(subparser): diff --git a/tilia/ui/cli/ui.py b/tilia/ui/cli/ui.py index e4bcd6762..b5c33d358 100644 --- a/tilia/ui/cli/ui.py +++ b/tilia/ui/cli/ui.py @@ -111,7 +111,7 @@ def parse_and_run(self, cmd): def run(self, cmd: str) -> bool: """ Parses the commands entered by the user. - Return True if an uncaught exception ocurred. + Return True if an uncaught exception occurred. The exception is stored in self.exception. """ try: @@ -150,6 +150,10 @@ def get_player_class(media_type: str): def show_crash_dialog(exc_message) -> None: post(Post.DISPLAY_ERROR, "CLI has crashed", "Error: " + exc_message) + @staticmethod + def exit(code: int): + raise SystemExit(code) + def on_ask_yes_or_no(title: str, prompt: str) -> bool: return ask_yes_or_no(f"{title}: {prompt}") diff --git a/tilia/ui/qtui.py b/tilia/ui/qtui.py index e8248cab4..bd610d7f1 100644 --- a/tilia/ui/qtui.py +++ b/tilia/ui/qtui.py @@ -313,7 +313,7 @@ def launch(self): return self.q_application.exec() def exit(self, code: int): - # Code = 0 means a succesful run, code = 1 means an unhandled exception. + # Code = 0 means a successful run, code = 1 means an unhandled exception. self.q_application.exit(code) def get_window_geometry(self): @@ -416,7 +416,7 @@ def on_media_load_youtube(): def on_clear_ui(self): """Closes all UI windows.""" - for kind, window in self._windows.items(): + for window in self._windows.values(): if window is not None: window.close() self.main_window.setFocus() diff --git a/tilia/ui/timelines/base/element.py b/tilia/ui/timelines/base/element.py index 622ec4fcc..4cb4b5a34 100644 --- a/tilia/ui/timelines/base/element.py +++ b/tilia/ui/timelines/base/element.py @@ -64,7 +64,7 @@ def is_selected(self): return self in self.timeline_ui.selected_elements @abstractmethod - def child_items(self): + def child_items(self) -> list[Any]: ... def selection_triggers(self): @@ -86,12 +86,18 @@ def on_right_click(self, x, y, _): menu = self.CONTEXT_MENU_CLASS(self) menu.exec(QPoint(x, y)) + @abstractmethod def on_select(self): ... + @abstractmethod def on_deselect(self): ... + @abstractmethod + def update_position(self): + ... + def delete(self): for item in self.child_items(): if item.parentItem(): diff --git a/tilia/ui/timelines/base/element_manager.py b/tilia/ui/timelines/base/element_manager.py index cb4360c8f..4a331b27e 100644 --- a/tilia/ui/timelines/base/element_manager.py +++ b/tilia/ui/timelines/base/element_manager.py @@ -70,10 +70,10 @@ def _remove_from_elements_set(self, element: TE) -> None: try: self._elements.remove(element) del self.id_to_element[element.id] - except ValueError: + except ValueError as e: raise ValueError( f"Can't remove element '{element}' from {self}: not in self._elements." - ) + ) from e def get_element(self, id: int) -> TE: return self.id_to_element[id] @@ -173,11 +173,11 @@ def _add_to_selected_elements_set(self, element: TE) -> None: def _remove_from_selected_elements_set(self, element: TE) -> None: try: self._selected_elements.remove(element) - except ValueError: + except ValueError as e: raise ValueError( f"Can't remove element '{element}' from selected objects of {self}: not" " in self._selected_elements." - ) + ) from e def get_selected_elements(self) -> list[TE]: return self._selected_elements diff --git a/tilia/ui/timelines/base/timeline.py b/tilia/ui/timelines/base/timeline.py index 104a8788c..8d011197f 100644 --- a/tilia/ui/timelines/base/timeline.py +++ b/tilia/ui/timelines/base/timeline.py @@ -538,7 +538,7 @@ def delete_element(self, element: T): self.element_manager.delete_element(element) def validate_copy(self, elements: list[T]) -> None: - """Can be overwritten by subclsses""" + """Can be overwritten by subclasses""" def validate_paste( self, paste_data: dict, elements_to_receive_paste: list[T] diff --git a/tilia/ui/timelines/beat/element.py b/tilia/ui/timelines/beat/element.py index 41a430862..164d93213 100644 --- a/tilia/ui/timelines/beat/element.py +++ b/tilia/ui/timelines/beat/element.py @@ -115,7 +115,7 @@ def seek_time(self): return self.time def child_items(self): - return self.body, self.label + return [self.body, self.label] def update_time(self): self.update_position() @@ -133,16 +133,16 @@ def update_label(self): self.label.set_position(self.x, self.label_y) def selection_triggers(self): - return self.body, self.label + return [self.body, self.label] def left_click_triggers(self): - return (self.body,) + return [self.body] def on_left_click(self, _) -> None: self.setup_drag() def double_left_click_triggers(self): - return self.body, self.label + return [self.body, self.label] def on_double_left_click(self, _) -> None: if self.drag_manager: @@ -150,9 +150,8 @@ def on_double_left_click(self, _) -> None: self.drag_manager = None post(Post.PLAYER_SEEK, self.seek_time) - @property def right_click_triggers(self): - return self.body, self.label + return [self.body, self.label] def setup_drag(self): self.drag_manager = DragManager( @@ -266,7 +265,7 @@ def set_position(self, x, y): def set_text(self, value: str): if not value: - # Settting plain text to empty string + # Setting plain text to empty string # keeps the graphics item interactable, # so we need to hide it, instead. self.setVisible(False) diff --git a/tilia/ui/timelines/beat/timeline.py b/tilia/ui/timelines/beat/timeline.py index 78597faab..c895e3186 100644 --- a/tilia/ui/timelines/beat/timeline.py +++ b/tilia/ui/timelines/beat/timeline.py @@ -275,7 +275,7 @@ def update_measure_numbers(self): # State is being restored and # beats in measure has not been # updated yet. This is a dangerous - # workaroung, as it might conceal + # workaround, as it might conceal # other exceptions. Let's fix this ASAP. continue diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index c454c71f9..e49acdf5e 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -569,11 +569,11 @@ def _add_to_timeline_uis_set(self, timeline_ui: TimelineUI) -> None: def _remove_from_timeline_uis_set(self, timeline_ui: TimelineUI) -> None: try: self._timeline_uis.remove(timeline_ui) - except ValueError: + except ValueError as e: raise ValueError( f"Can't remove timeline ui '{timeline_ui}' from {self}: not in" " self.timeline_uis." - ) + ) from e def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None: self._select_order.insert(0, tl_ui) @@ -581,18 +581,18 @@ def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None: def _remove_from_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None: try: self._select_order.remove(tl_ui) - except ValueError: + except ValueError as e: raise ValueError( f"Can't remove timeline ui '{tl_ui}' from select order: not in select" " order." - ) + ) from e def _send_to_top_of_select_order(self, tl_ui: TimelineUI): self._select_order.remove(tl_ui) self._select_order.insert(0, tl_ui) def add_timeline_view_to_scene(self, view: QGraphicsView, ordinal: int) -> None: - view.proxy = self.scene.addWidget(view) + self.scene.addWidget(view) y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1]) view.move(0, y) self.update_height() @@ -1121,8 +1121,8 @@ def filter_for_pasting(_) -> list[TimelineUI]: try: return selector_to_func[selector](get_by_kinds(kinds)) - except KeyError: - raise NotImplementedError(f"Can't select with {selector=}") + except KeyError as e: + raise NotImplementedError(f"Can't select with {selector=}") from e @command_callback def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI): diff --git a/tilia/ui/timelines/copy_paste.py b/tilia/ui/timelines/copy_paste.py index 5dcc8ac98..a25886d2d 100644 --- a/tilia/ui/timelines/copy_paste.py +++ b/tilia/ui/timelines/copy_paste.py @@ -11,7 +11,7 @@ def get_copy_data_from_elements( elements: list[tuple[TimelineUIElement, CopyAttributes]], ) -> list[dict]: copy_data = [] - for element, kind, copy_attrs in elements: + for element, copy_attrs in elements: copy_data.append(get_copy_data_from_element(element, copy_attrs)) return copy_data diff --git a/tilia/ui/timelines/hierarchy/element.py b/tilia/ui/timelines/hierarchy/element.py index 1948b98c6..20f851e2e 100644 --- a/tilia/ui/timelines/hierarchy/element.py +++ b/tilia/ui/timelines/hierarchy/element.py @@ -76,7 +76,7 @@ class HierarchyUI(TimelineUIElement): support_by_component_value=["start", "pre_start", "end", "level"], ) - NAME_WHEN_UNLABELED = "Unnamed" + NAME_WHEN_UNLABELLED = "Unnamed" FULL_NAME_SEPARATOR = "-" UPDATE_TRIGGERS = [ @@ -188,12 +188,12 @@ def get_cropped_label(self, start_x, end_x, label): @property def full_name(self) -> str: - partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELED + partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELLED next_parent = self.get_data("parent") while next_parent: - parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELED + parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELLED partial_name = parent_name + self.FULL_NAME_SEPARATOR + partial_name next_parent = next_parent.parent @@ -454,8 +454,8 @@ def extremity_to_handle( HierarchyUI.Extremity.PRE_START: self.pre_start_handle, HierarchyUI.Extremity.POST_END: self.post_end_handle, }[extremity] - except KeyError: - raise ValueError("Unrecognized extremity") + except KeyError as e: + raise ValueError("Unrecognized extremity") from e @staticmethod def frame_to_body_extremity( @@ -468,8 +468,8 @@ def frame_to_body_extremity( HierarchyUI.Extremity.PRE_START: HierarchyUI.Extremity.START, HierarchyUI.Extremity.POST_END: HierarchyUI.Extremity.END, }[extremity] - except KeyError: - raise ValueError("Unrecognized extremity") + except KeyError as e: + raise ValueError("Unrecognized extremity") from e def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle): try: @@ -479,8 +479,8 @@ def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle self.pre_start_handle: HierarchyUI.Extremity.PRE_START, self.post_end_handle: HierarchyUI.Extremity.POST_END, }[handle] - except KeyError: - raise ValueError(f"{handle} if not a handle of {self}") + except KeyError as e: + raise ValueError(f"{handle} if not a handle of {self}") from e @staticmethod def extremity_to_x(extremity: HierarchyUI.Extremity, start_x, end_x): @@ -501,7 +501,7 @@ def _setup_handle(self, extremity: HierarchyUI.Extremity): ) def selection_triggers(self): - return self.body, self.label, self.comments_icon + return [self.body, self.label, self.comments_icon] def left_click_triggers(self): triggers = [ @@ -531,7 +531,7 @@ def on_double_left_click(self, _) -> None: post(Post.PLAYER_SEEK, self.seek_time) def right_click_triggers(self): - return self.body, self.label, self.comments_icon + return [self.body, self.label, self.comments_icon] def on_select(self) -> None: self.body.on_select() @@ -554,7 +554,7 @@ def on_deselect(self) -> None: def selected_ascendants(self) -> list[HierarchyUI]: """Returns hierarchies in the same branch that - are both selected and higher-leveled than self""" + are both selected and higher-levelled than self""" uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x) selected_uis = self.timeline_ui.selected_elements @@ -566,7 +566,7 @@ def selected_ascendants(self) -> list[HierarchyUI]: def selected_descendants(self) -> list[HierarchyUI]: """Returns hierarchies in the same branch that are both - selected and lower-leveled than self""" + selected and lower-levelled than self""" uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x) selected_uis = self.timeline_ui.selected_elements diff --git a/tilia/ui/timelines/pdf/timeline.py b/tilia/ui/timelines/pdf/timeline.py index 5f83a47b8..d9245cf0e 100644 --- a/tilia/ui/timelines/pdf/timeline.py +++ b/tilia/ui/timelines/pdf/timeline.py @@ -99,7 +99,7 @@ def page_total(self): # prevents it from capping the # number at a value that might be lower than # the page total in a PDF loaded in the future. - # Can't use math.inf because PyQt requires an int. + # Can't use math.inf because pyside requires an int. return 99999999 return self.timeline.get_data("page_total") diff --git a/tilia/ui/timelines/score/element/barline.py b/tilia/ui/timelines/score/element/barline.py index f62cb3d57..f277087ee 100644 --- a/tilia/ui/timelines/score/element/barline.py +++ b/tilia/ui/timelines/score/element/barline.py @@ -12,6 +12,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.body = None + @property def x(self): return time_x_converter.get_x_by_time(self.get_data("time")) @@ -23,7 +24,7 @@ def child_items(self): def get_body_args(self): return ( - self.x(), + self.x, self.timeline_ui.staff_y_cache.values(), ) @@ -41,6 +42,12 @@ def on_components_deserialized(self): def selection_triggers(self): return [] + def on_deselect(self): + return + + def on_select(self): + return + class BarLineBody: def __init__(self, x, ys: list[tuple[float, float]]): @@ -56,5 +63,5 @@ def _setup_lines(self, x: float, ys: list[tuple[float, float]]): self.set_position(x, ys) def set_position(self, x: float, ys: list[tuple[float, float]]): - for line, (y0, y1) in zip(self.lines, ys): + for line, (y0, y1) in zip(self.lines, ys, strict=True): line.setLine(x, y0, x, y1) diff --git a/tilia/ui/timelines/score/element/clef.py b/tilia/ui/timelines/score/element/clef.py index cf5814ee3..f671df1f8 100644 --- a/tilia/ui/timelines/score/element/clef.py +++ b/tilia/ui/timelines/score/element/clef.py @@ -61,6 +61,12 @@ def selection_triggers(self): def shorthand(self) -> Clef.Shorthand | None: return self.tl_component.shorthand() + def on_deselect(self): + return + + def on_select(self): + return + class ClefBody(QGraphicsPixmapItem): def __init__(self, x: float, y: float, height: float, path: Path): diff --git a/tilia/ui/timelines/score/element/key_signature.py b/tilia/ui/timelines/score/element/key_signature.py index baa64b080..ff6520d6a 100644 --- a/tilia/ui/timelines/score/element/key_signature.py +++ b/tilia/ui/timelines/score/element/key_signature.py @@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs): @staticmethod def _clef_shorthand_to_icon_path_string(shorthand: Clef.Shorthand | None) -> str: if not shorthand: - # Key signature not implmemented + # Key signature not implemented # for custom clefs. Using "treble" # just to prevent a crash return "treble" @@ -84,6 +84,12 @@ def on_components_deserialized(self): self.scene.removeItem(self.body) self._setup_body() + def on_deselect(self): + return + + def on_select(self): + return + class KeySignatureBody(QGraphicsPixmapItem): def __init__(self, x: float, y: float, height: int, path: str): diff --git a/tilia/ui/timelines/score/element/note/ui.py b/tilia/ui/timelines/score/element/note/ui.py index f09e31d09..684dc21cc 100644 --- a/tilia/ui/timelines/score/element/note/ui.py +++ b/tilia/ui/timelines/score/element/note/ui.py @@ -242,20 +242,20 @@ def get_accidental_height(accidental: int, scale_factor: float) -> int: def get_accidental_scale_factor(self): """ Scales accidental according to amw = average measure width. - If amw < visibility_treshold, returns 0, indicating accidentals should be hidden. - If visibility_treshold < amw < max_size_treshold, scales proportionally with min_scale as a minimum. - If amw > max_size_treshold, returns 1, indicating accidentals should be fully visible. + If amw < visibility_threshold, returns 0, indicating accidentals should be hidden. + If visibility_threshold < amw < max_size_threshold, scales proportionally with min_scale as a minimum. + If amw > max_size_threshold, returns 1, indicating accidentals should be fully visible. """ - visibility_treshold = 30 - max_size_treshold = 180 + visibility_threshold = 30 + max_size_threshold = 180 min_scale = 0.5 average_measure_width = self.timeline_ui.average_measure_width() if not average_measure_width: return 1 - if average_measure_width < visibility_treshold: + if average_measure_width < visibility_threshold: return 0 return min( - 1, min_scale + (average_measure_width / max_size_treshold * min_scale) + 1, min_scale + (average_measure_width / max_size_threshold * min_scale) ) def update_color(self): diff --git a/tilia/ui/timelines/score/element/staff.py b/tilia/ui/timelines/score/element/staff.py index 87007a054..05d31bcf5 100644 --- a/tilia/ui/timelines/score/element/staff.py +++ b/tilia/ui/timelines/score/element/staff.py @@ -51,6 +51,12 @@ def child_items(self): def selection_triggers(self): return [] + def on_deselect(self): + return + + def on_select(self): + return + class StaffLines: COLOR = "gray" diff --git a/tilia/ui/timelines/score/element/time_signature.py b/tilia/ui/timelines/score/element/time_signature.py index f31f7cfcc..0443f6fa5 100644 --- a/tilia/ui/timelines/score/element/time_signature.py +++ b/tilia/ui/timelines/score/element/time_signature.py @@ -68,6 +68,12 @@ def on_components_deserialized(self): def selection_triggers(self): return [] + def on_deselect(self): + return + + def on_select(self): + return + class TimeSignatureBody(QGraphicsItem): def __init__( @@ -96,7 +102,7 @@ def get_scaled_pixmap(self, digit: int | str, height: int): def set_numerator_items(self, numerator: int, height: int): self.numerator_items = [] for i, digit in enumerate(str(numerator)): - item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self) + item = NumberPixmap(self.get_scaled_pixmap(digit, height), self) item.digit = int(digit) item.setPos(i * item.pixmap().width(), 0) self.numerator_items.append(item) @@ -104,7 +110,7 @@ def set_numerator_items(self, numerator: int, height: int): def set_denominator_items(self, denominator: int, height: int): self.denominator_items = [] for i, digit in enumerate(str(denominator)): - item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self) + item = NumberPixmap(self.get_scaled_pixmap(digit, height), self) item.digit = int(digit) item.setPos(i * item.pixmap().width(), item.pixmap().height()) self.denominator_items.append(item) @@ -145,5 +151,6 @@ def boundingRect(self): bounding_rect = bounding_rect.united(item.boundingRect()) return bounding_rect - def paint(self, painter, option, widget): - ... + +class NumberPixmap(QGraphicsPixmapItem): + digit = 0 diff --git a/tilia/ui/timelines/score/timeline.py b/tilia/ui/timelines/score/timeline.py index b7739f1b9..301fc2943 100644 --- a/tilia/ui/timelines/score/timeline.py +++ b/tilia/ui/timelines/score/timeline.py @@ -448,8 +448,8 @@ def _validate_staff_numbers(self) -> bool: def average_measure_width(self) -> float: if self._measure_count == 0: return 0 - x0 = self.first_bar_line.x() - x1 = self.last_bar_line.x() + x0 = self.first_bar_line.x + x1 = self.last_bar_line.x return (x1 - x0) / self._measure_count def on_audio_time_change(self, time: float, _) -> None: diff --git a/tilia/ui/windows/settings.py b/tilia/ui/windows/settings.py index b6e648e3c..afb0d81e9 100644 --- a/tilia/ui/windows/settings.py +++ b/tilia/ui/windows/settings.py @@ -195,7 +195,7 @@ def pretty_label(input_string: str): ) -def select_color_button(value, text=None): +def select_color_button(value, text=""): def select_color(old_color): new_color = QColorDialog.getColor( QColor(old_color), @@ -212,7 +212,7 @@ def set_color(color): ) def get_value(): - return button.styleSheet().lstrip("background-color: ").split(";")[0] + return button.styleSheet().replace("background-color: ", "").split(";")[0] button = QPushButton() button.setText(text) @@ -231,7 +231,7 @@ def combobox(options: list, current_value: str): return combobox -def get_widget_for_value(value, text=None) -> QWidget: +def get_widget_for_value(value, text="") -> QWidget: match value: case bool(): checkbox = QCheckBox() @@ -284,7 +284,15 @@ def get_widget_for_value(value, text=None) -> QWidget: raise NotImplementedError -def get_value_for_widget(widget: QWidget): +def get_value_for_widget( + widget: QSpinBox + | QTextEdit + | QWidget + | QCheckBox + | QPushButton + | QComboBox + | QLineEdit, +): match widget.objectName(): case "int": return widget.value() @@ -304,7 +312,7 @@ def get_value_for_widget(widget: QWidget): return widget.isChecked() case "color": - return widget.styleSheet().lstrip("background-color: ").split(";")[0] + return widget.styleSheet().replace("background-color: ", "").split(";")[0] case "combobox": text = widget.currentText().lower() From a6d1b1aeef57c218f3c977f74ddf1135b6888fd4 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:17:50 +0000 Subject: [PATCH 36/75] ci: don't test python 3.13 on windows no support --- .github/workflows/run-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ad64908e2..40ee4a07a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,6 +33,9 @@ jobs: path: ~/Library/Caches/pip - os: windows-latest path: ~\AppData\Local\pip\Cache + exclude: + - os: windows-latest + python-version: '3.13' # no pyside support timeout-minutes: 30 env: QT_SELECT: "qt6" From 2d57cdd1c3f75f48c8e040654c2f459f0df42949 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:18:30 +0000 Subject: [PATCH 37/75] chore: update docs --- README.md | 28 ++++++++++++---------------- TESTING.md | 2 ++ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0c54e1489..774e7c663 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,9 @@ Before you start, you will need: | `git` | Download [here](https://git-scm.com/install) | Not necessary for a direct download from this repository | -### Note for Linux users +### Note to Linux users Users have reported dependency issues when running TiLiA on Linux (see [#370](https://github.com/TimeLineAnnotator/desktop/issues/370) and [#371](https://github.com/TimeLineAnnotator/desktop/issues/371)). -That is probably due to `PyInstaller` not being completely compatible with `PyQt`. -We are working to fix that with with a new build process using `pyside6-deploy` instead. +Due to system variations between Linux distributions, some additional system dependencies may be required. Visit our [help page](https://tilia-app.com/help/installation#troubleshooting-linux) for more information. ### Running from source @@ -81,17 +80,12 @@ Change directory to the cloned repository: cd tilia-desktop ``` Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps. -Failure to do so is likely to cause issues with dependencies. +Failure to do so may cause issues with dependencies. Install TiLiA and its dependencies with: ``` pip install -e . ``` -On Linux, some additional Qt dependencies are required: -``` -sudo apt install libnss3 libasound libxkbfile1 libpulse0 -``` -| n.b.: the specific names of these packages varies based on your Linux distribution. To run TiLiA from source, run: ``` @@ -104,18 +98,20 @@ python -m tilia --user-interface cli ``` ### Building from source -TiLiA uses [PyInstaller](https://pyinstaller.org/en/stable/) to build binaries. -Note that the binaries will be for the platform you are building on, as `PyInstaller` supports no cross-compilation. +TiLiA uses [Nuitka](https://nuitka.net/) to build binaries. +Note that the binaries will be for the platform you are building on, as `Nuitka` supports no cross-compilation. -After cloning tilia and installing the dependencies (see above), install `PyInstaller` with: +After cloning TiLiA, install TiLiA's run and build dependencies with: ``` -pip install pyinstaller +pip install -e . --group build ``` -To build a stand-alone executable, run PyInstaller with the settings in `tilia.spec`: +To build a stand-alone executable, run the script: ``` -pyinstaller tilia.spec +python scripts/deploy.py [ref_name] [os_type] ``` -The executable will be created in `dist` folder inside the project directory. +(*n.b.: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.) + +The executable will be found in the `build/[os_type]/exe` folder in the project directory. ## Planned features diff --git a/TESTING.md b/TESTING.md index 8593df908..b371ef7f5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,4 +1,6 @@ The test suite is written in pytest. Below are some things to keep in my mind when writing tests. For examples of good and thorough tests, see `tests\ui\timelines\test_marker_timeline_ui.py`. Older modules should be refactored at some point to follow the guidelines below. +## Pre-requisites +`pip install --group testing` ## How to simulate interaction with the UI? - The `user_actions` fixture can be used to trigger actions on the UI. This is equivalent to pressing buttons on the UI. We should also check that the actions are available in the UI where we expect them. - The `tilia_state` fixture can be used to make certain changes to state simulating user input (e.g. `tilia_state.current_time = 10`). From f7b0c91ca5e97d7986ae843de775cda17b325bc8 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:54:23 +0000 Subject: [PATCH 38/75] fix: locating start of script --- pyproject.toml | 2 +- tilia/__main__.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87519f383..0e7b5d204 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ name = "Felipe Defensor et al." email = "tilia@tilia-app.com" [project.gui-scripts] -tilia-desktop = "tilia.__main__:boot" +tilia-desktop = "tilia.__main__:main" [project.urls] Homepage = "https://tilia-app.com" diff --git a/tilia/__main__.py b/tilia/__main__.py index 544386887..9b6c5dba8 100644 --- a/tilia/__main__.py +++ b/tilia/__main__.py @@ -46,7 +46,7 @@ def deps_debug(exc: ImportError): raise RuntimeError("Install the necessary dependencies then restart.") from exc -if __name__ == "__main__": +def main(): try: from tilia.boot import boot # noqa: E402 @@ -55,3 +55,7 @@ def deps_debug(exc: ImportError): if sys.platform != "linux": raise exc deps_debug(exc) + + +if __name__ == "__main__": + main() From 1dcdd73fed49a325a88ca1513ec5308dc44ce6a9 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:56:32 +0000 Subject: [PATCH 39/75] fix: typing --- tilia/ui/timelines/collection/collection.py | 17 ++++++++++------- tilia/ui/timelines/view.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index e49acdf5e..28be3610d 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -7,7 +7,6 @@ from PySide6.QtCore import Qt, QPoint from PySide6.QtWidgets import ( - QGraphicsView, QMainWindow, QGraphicsItem, QGraphicsScene, @@ -42,6 +41,8 @@ from tilia.ui.timelines.view import TimelineView from .view import TimelineUIsView from ..base.element import TimelineUIElement +from ..beat import BeatTimelineUI +from ..harmony import HarmonyTimelineUI from ..selection_box import SelectionBoxQt from ..slider.timeline import SliderTimelineUI from ...dialogs.add_timeline_without_media import AddTimelineWithoutMedia @@ -74,7 +75,7 @@ def __init__( kind: None for kind in TimelineKind if kind != TlKind.SLIDER_TIMELINE } - self._timeline_uis = set() + self._timeline_uis: set[TimelineUI] = set() self._select_order = [] self._timeline_uis_to_playback_line_ids = {} self.sb_items_to_selected_items = {} @@ -591,7 +592,8 @@ def _send_to_top_of_select_order(self, tl_ui: TimelineUI): self._select_order.remove(tl_ui) self._select_order.insert(0, tl_ui) - def add_timeline_view_to_scene(self, view: QGraphicsView, ordinal: int) -> None: + def add_timeline_view_to_scene(self, view: TimelineView, ordinal: int) -> None: + view.proxy = self.scene.addWidget(view) self.scene.addWidget(view) y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1]) view.move(0, y) @@ -646,7 +648,7 @@ def update_timeline_ui_ordinal(self): @staticmethod def update_timeline_times(tlui: TimelineUI): if tlui.TIMELINE_KIND == TlKind.SLIDER_TIMELINE: - tlui: SliderTimelineUI + tlui = cast(SliderTimelineUI, tlui) tlui.update_items_position() else: tlui.element_manager.update_time_on_elements() @@ -693,7 +695,7 @@ def _get_timeline_ui_by_view(self, view): def _on_timeline_ui_right_click( self, - view: QGraphicsView, + view: TimelineView, x: int, y: int, item: Optional[QGraphicsItem], @@ -718,7 +720,7 @@ def _on_timeline_ui_right_click( def _on_timeline_ui_left_click( self, - view: QGraphicsView, + view: TimelineView, x: int, y: int, item: Optional[QGraphicsItem], @@ -915,7 +917,8 @@ def on_hierarchy_merge_split(self, new_units: list, old_units: list): self._update_loop_elements() def on_harmony_timeline_components_deserialized(self, id): - self.get_timeline_ui(id).on_timeline_components_deserialized() # noqa + timeline_ui = cast(HarmonyTimelineUI, self.get_timeline_ui(id)) + timeline_ui.on_timeline_components_deserialized() def on_beat_timeline_components_deserialized(self, id: int): from tilia.ui.timelines.beat import BeatTimelineUI diff --git a/tilia/ui/timelines/view.py b/tilia/ui/timelines/view.py index 5fe292fc9..a0393b0e6 100644 --- a/tilia/ui/timelines/view.py +++ b/tilia/ui/timelines/view.py @@ -8,7 +8,13 @@ QColor, QBrush, ) -from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QSizePolicy, QFrame +from PySide6.QtWidgets import ( + QFrame, + QGraphicsProxyWidget, + QGraphicsScene, + QGraphicsView, + QSizePolicy, +) from tilia.settings import settings from tilia.requests import post, Post, Get, get, listen @@ -39,7 +45,7 @@ def __init__(self, scene: QGraphicsScene): ) self.dragging = False - self.proxy = None # will be set by TimelineUIs + self.proxy = QGraphicsProxyWidget() # will be set by TimelineUIs def on_settings_updated(self, updated_settings): if "general" in updated_settings: From 4e5f874be84dcce6f6568e8da2cc8a11e8621bc0 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:58:28 +0000 Subject: [PATCH 40/75] fix: stdout not showing this is apparently a windows specific difference --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0e7b5d204..cbdddfcd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ name = "Felipe Defensor et al." email = "tilia@tilia-app.com" -[project.gui-scripts] +[project.scripts] tilia-desktop = "tilia.__main__:main" [project.urls] From ec00b76f8793791a03ffdf2b09c3dd8aa93daa32 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:59:40 +0000 Subject: [PATCH 41/75] chore: shorten name --- README.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 774e7c663..3570e4aa4 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,12 @@ pip install -e . To run TiLiA from source, run: ``` -tilia-desktop +tilia ``` TiLiA also offers a CLI mode, which can be run with: ``` -python -m tilia --user-interface cli +tilia --user-interface cli ``` ### Building from source diff --git a/pyproject.toml b/pyproject.toml index cbdddfcd0..19346394b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ name = "Felipe Defensor et al." email = "tilia@tilia-app.com" [project.scripts] -tilia-desktop = "tilia.__main__:main" +tilia = "tilia.__main__:main" [project.urls] Homepage = "https://tilia-app.com" From d06509ddb24f50600e893d62820941e016445d9c Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:02:19 +0000 Subject: [PATCH 42/75] chore: bump year --- tilia/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilia/constants.py b/tilia/constants.py index 6468db56d..6324dfc98 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -30,7 +30,7 @@ APP_NAME = setupcfg.get("name", "") VERSION = setupcfg.get("version", "0.0.0") -YEAR = "2022-2025" +YEAR = "2022-2026" FILE_EXTENSION = "tla" EMAIL_URL = "mailto:" + EMAIL From 6f65b2b290fc05db4be3ffc686f09dbf33a0353e Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:16:58 +0000 Subject: [PATCH 43/75] docs: update and fix some typing... --- tilia/media/player/qtplayer.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py index 2ddef4eb9..c158af543 100644 --- a/tilia/media/player/qtplayer.py +++ b/tilia/media/player/qtplayer.py @@ -16,19 +16,28 @@ def wait_for_signal(signal: SignalInstance, value): + """ + Many Qt functions run on threads, and this wrapper makes sure that after starting a process, the right signal is emitted before continuing the TiLiA process. + See _engine_stop of QtPlayer for an example implementation. + + :param signal: The signal to watch. + :type signal: SignalInstance + :param value: The "right" output value that signal should emit before continuing. (eg. on stopping player, playbackStateChanged emits StoppedState when player has been successfully stopped. Only then can we continue the rest of the update process.) + """ + def signal_wrapper(func): timer = QTimer(singleShot=True, interval=100) loop = QEventLoop() - success = False # noqa: F841 + success = False def value_checker(signal_value): if signal_value == value: - global success + nonlocal success success = True loop.quit() def check_signal(*args, **kwargs): - global success + nonlocal success if not func(*args, **kwargs): return False signal.connect(value_checker) From 80cb886c218839e2d6fbc431c4335596795899aa Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:30:32 +0000 Subject: [PATCH 44/75] docs: update deploy script --- scripts/deploy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/deploy.py b/scripts/deploy.py index 5af75a236..adc0d91ee 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -50,8 +50,11 @@ def _handle_inputs(): assert len(sys.argv) == 3, "Incorrect number of inputs" global ref_name, build_os, outdir ref_name = sys.argv[1] + # in a git action runner, sys.argv[2] could look like someOS-latest, someOS-22.02, etc, where someOS is probably macos, ubuntu or windows. + # we save build_os as the runner os stripped of "latest" and any digits: just someOS. if "macos" in sys.argv[2] and "intel" not in sys.argv[2]: build_os = "macos-silicon" + # to identify the difference between macos-silicon and macos-intel. currently uses the images macos-latest and macos-15-intel (which are silicon and intel respectively.) else: build_os = "-".join( [ From da796bf190dfa66a1fab96fda7b4e91a7c73be0c Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:33:43 +0000 Subject: [PATCH 45/75] ci: more descriptive build-test --- .github/workflows/build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e25ac6d90..c0fec5358 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,14 @@ jobs: run: | brew install coreutils echo "Checking dependencies" + # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is less than 10s. + # then zip everything up and delete the original non-zipped file. + start=$(date +%s) echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true + if (($(date +%s) - $start < 10)); then + echo "::error::Process terminated early! Potential errors in build." + exit 1 + fi zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true rm -rf ${{ steps.build-exe.outputs.out-filepath }} @@ -74,7 +81,13 @@ jobs: shell: bash run: | echo "Checking dependencies" - echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s. + start=$(date +%s) + echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + if (($(date +%s) - $start < 10)); then + echo "::error::Process terminated early! Potential errors in build." + exit 1 + fi - name: Test executable [Linux] id: set-path @@ -83,7 +96,13 @@ jobs: run: | echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT" echo "Checking dependencies" - echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s. + start=$(date +%s) + echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true + if (($(date +%s) - $start < 10)); then + echo "::error::Process terminated early! Potential errors in build." + exit 1 + fi - name: Upload executable uses: actions/upload-artifact@v4 From 7fc79e8efbbec67a35ca94568a4026a20df068ff Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:00:38 +0000 Subject: [PATCH 46/75] fix: accidental duplicate --- tilia/ui/timelines/collection/collection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index 28be3610d..05ae297f4 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -594,7 +594,6 @@ def _send_to_top_of_select_order(self, tl_ui: TimelineUI): def add_timeline_view_to_scene(self, view: TimelineView, ordinal: int) -> None: view.proxy = self.scene.addWidget(view) - self.scene.addWidget(view) y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1]) view.move(0, y) self.update_height() From f7e3fd54251cf5788b182e212e6e42117b7d7605 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:13:11 +0000 Subject: [PATCH 47/75] fix: reverts 66e88dc --- tilia/dirs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tilia/dirs.py b/tilia/dirs.py index e2956efd0..762002b93 100644 --- a/tilia/dirs.py +++ b/tilia/dirs.py @@ -37,6 +37,10 @@ def setup_logs_path(data_dir): def setup_dirs() -> None: + # if not in prod, set directory to root of tilia + if os.environ.get("ENVIRONMENT") != "prod": + os.chdir(os.path.dirname(__file__)) + data_dir = setup_data_dir() global autosaves_path, logs_path From a1d390f7357c437ca5f12820d632f74e1ea437f6 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:19:48 +0000 Subject: [PATCH 48/75] docs: some help on nuitka-package.config --- tilia.nuitka-package.config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml index b4042cbe0..e4c7429ad 100644 --- a/tilia.nuitka-package.config.yml +++ b/tilia.nuitka-package.config.yml @@ -1,10 +1,11 @@ +# see https://nuitka.net/user-documentation/nuitka-package-config.html for help on this file. # yamllint disable rule:line-length # yamllint disable rule:indentation # yamllint disable rule:comments-indentation # yamllint disable rule:comments # too many spelling things, spell-checker: disable --- -- module-name: "music21" +- module-name: "music21" # mostly regex matching entire refrences to tests and deleting. anti-bloat: - description: "remove tests" replacements_plain: From 21e8617e4422496d5942ae7ce9841b5d739f4fe7 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:35:52 +0000 Subject: [PATCH 49/75] docs: script/deploy --- scripts/deploy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/deploy.py b/scripts/deploy.py index adc0d91ee..b7ec4cc4c 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -1,3 +1,15 @@ +""" +Script to build TiLiA with Nuitka. +(a more flexible alternative to building with only pyside-deploy or Nuitka) +pyside-deploy has very limited Nuitka-specific options and Nuitka requires a specific file structure to build. +Hence this pyside-deploy-inspired script. + +- Run `python scripts/deploy.py [ref_name] [os_type]` +- Creates sdist to create metadata file from information in pyproject and filter out unnecessary files +- Then builds executable, which will be found in: + build/exe/[os_type]/TiLiA-[tilia version in pyproject](-[ref_name, if not the same as tilia version])-[os_type] +""" + from colorama import Fore import dotenv from enum import Enum From 29aa7332204c1a191a9f5af321d3c5ba4917a1b0 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:37:36 +0000 Subject: [PATCH 50/75] ci: check ci test actually works --- tilia/boot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tilia/boot.py b/tilia/boot.py index 9c70ae093..58c696b98 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -2,6 +2,7 @@ import os import sys import traceback +import ImaginaryModule # noqa: F401 from PySide6.QtWidgets import QApplication From 16b8cf5613ff2b4fad8db331a3e3397603025261 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:29:11 +0000 Subject: [PATCH 51/75] ci: revert b4f9444 --- tilia/boot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tilia/boot.py b/tilia/boot.py index 58c696b98..9c70ae093 100644 --- a/tilia/boot.py +++ b/tilia/boot.py @@ -2,7 +2,6 @@ import os import sys import traceback -import ImaginaryModule # noqa: F401 from PySide6.QtWidgets import QApplication From a29f0432d7508c57a982b43db8657fbf164b1e38 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:10:02 +0000 Subject: [PATCH 52/75] ci: pin setuptools version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 19346394b..991a86499 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ build = [ "Nuitka >= 4.0", "ordered-set==4.1.0", "pyyaml", - "setuptools; python_version >= '3.12'", + "setuptools<82.0.0; python_version >= '3.12'", "wheel==0.38.4", "zstandard==0.20.0" ] From 24c42d4f2f05d9a3f80cb255f8a521a1acb734a4 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:25:45 +0000 Subject: [PATCH 53/75] fix: settings fix uninstantiated error --- tilia/settings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tilia/settings.py b/tilia/settings.py index 373dc40c4..ef597e3fb 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -89,7 +89,9 @@ class SettingsManager(QObject): } def __init__(self): - self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings", None) + self._settings = QSettings( + tilia.constants.APP_NAME, application="Desktop Settings", parent=None + ) self._files_updated_callbacks = set() self._cache = {} self._check_all_default_settings_present() @@ -118,7 +120,10 @@ def link_file_update(self, updating_function) -> None: def _get(self, group_name: str, setting: str, in_default=True): key = self._get_key(group_name, setting, in_default) - value = self._settings.value(key, None) + try: + value = self._settings.value(key, None) + except EOFError: + value = None if not value or not isinstance( value, type(self.DEFAULT_SETTINGS[group_name][setting]) ): From 804562d545b44564b7557c7c6bccba010f0e6be0 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:59:41 +0000 Subject: [PATCH 54/75] ci: remove CLI from executable --- tilia.nuitka-package.config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml index e4c7429ad..6c5353d8c 100644 --- a/tilia.nuitka-package.config.yml +++ b/tilia.nuitka-package.config.yml @@ -49,3 +49,11 @@ - description: "slightly hacky way of forcing prod if env file is not found" replacements_plain: 'os.environ["ENVIRONMENT"] = "dev"': 'os.environ["ENVIRONMENT"] = "prod"' + +- module-name: "tilia.boot" + anti-bloat: + - description: "CLI doesn't currently work in the executable" + replacements_re: + 'elif interface == "cli":(\n|.)*?return CLI\(\)': '' + replacements_plain: + 'choices=["qt", "cli"]': 'choices=["qt"], help="CLI option is currently not available in the exe. Run from source instead."' From d47b9610d1a273d4fab6de7a4f092428fba70e5d Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:03:51 +0000 Subject: [PATCH 55/75] ci: test cli --- .github/workflows/build.yml | 21 ++++++++++++++++++--- .github/workflows/run-tests.yml | 6 +++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0fec5358..243afb478 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,12 +67,16 @@ jobs: echo "Checking dependencies" # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is less than 10s. # then zip everything up and delete the original non-zipped file. + echo "Starting GUI..." start=$(date +%s) echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true if (($(date +%s) - $start < 10)); then echo "::error::Process terminated early! Potential errors in build." exit 1 fi + echo "\n\nStarting CLI..." + echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true + zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true rm -rf ${{ steps.build-exe.outputs.out-filepath }} @@ -82,12 +86,15 @@ jobs: run: | echo "Checking dependencies" # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s. + echo "Starting GUI..." start=$(date +%s) echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true if (($(date +%s) - $start < 10)); then echo "::error::Process terminated early! Potential errors in build." exit 1 fi + echo "\n\nStarting CLI..." + echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true - name: Test executable [Linux] id: set-path @@ -97,12 +104,15 @@ jobs: echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT" echo "Checking dependencies" # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s. + echo "Starting GUI..." start=$(date +%s) echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true if (($(date +%s) - $start < 10)); then echo "::error::Process terminated early! Potential errors in build." exit 1 fi + echo "\n\nStarting CLI..." + echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true - name: Upload executable uses: actions/upload-artifact@v4 @@ -137,13 +147,18 @@ jobs: run: | if [ -e build/ubuntu/exe/${{ needs.build.outputs.linux-build }} ]; then - chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }}; - if echo "timeout 30 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash; + chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }} + echo "Starting GUI..." + start=$(date +%s) + echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash || true + if (($(date +%s) - $start < 10)); then - echo "Linux works in a clean environment!"; + echo "Linux started in a clean environment!"; else echo "Linux didn't work... Check libraries with previous step?"; fi; + echo "Starting CLI..." + echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }} --user-interface=cli" | bash || true else echo "file not found!"; fi diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 40ee4a07a..983ada4f3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -85,4 +85,8 @@ jobs: - name: Check Program shell: bash - run: timeout 10 tilia-desktop || true + run: | + echo "Starting GUI..." + timeout 10 tilia-desktop || true + echo "Starting CLI..." + timeout 10 tilia-desktop -i=cli|| true From 03ecb3c5f9de31628168f7149ac78a5505937f50 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:06:36 +0000 Subject: [PATCH 56/75] fix: correct mode "app" sets onefile to windows and linux, and bundle to mac. --- pyproject.toml | 1 + scripts/deploy.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 991a86499..ed647f979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ remove-output = true report = "compilation-report.xml" user-package-configuration-file = "tilia.nuitka-package.config.yml" deployment = true +mode = "app" [tool.pytest.ini_options] env = [ diff --git a/scripts/deploy.py b/scripts/deploy.py index b7ec4cc4c..8c21216cf 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -116,10 +116,6 @@ def _get_exe_cmd() -> list[str]: f"--windows-icon-from-ico={icon_path}", f"--linux-icon={icon_path}", ] - if "macos" in build_os: - exe_args.append("--mode=app") - else: - exe_args.append("--mode=onefile") return exe_args From 8a0e65295b1ffcc16d67256fc8a0d337e076a2ec Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:15:58 +0000 Subject: [PATCH 57/75] docs: update no cli in exe --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3570e4aa4..20cf428c6 100644 --- a/README.md +++ b/README.md @@ -69,47 +69,48 @@ Due to system variations between Linux distributions, some additional system dep ### Running from source -Clone TiLiA with: +- Clone TiLiA with: ``` git clone https://github.com/TimeLineAnnotator/desktop.git tilia-desktop ``` -Change directory to the cloned repository: +- Change directory to the cloned repository: ``` cd tilia-desktop ``` Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps. Failure to do so may cause issues with dependencies. -Install TiLiA and its dependencies with: +- Install TiLiA and its dependencies with: ``` pip install -e . ``` -To run TiLiA from source, run: +- To run TiLiA from source, run: ``` tilia ``` -TiLiA also offers a CLI mode, which can be run with: +- TiLiA also offers a CLI mode, which can be run with: ``` tilia --user-interface cli ``` +Note: The CLI is currently only available when run from source, and not in the compiled executable. ### Building from source TiLiA uses [Nuitka](https://nuitka.net/) to build binaries. Note that the binaries will be for the platform you are building on, as `Nuitka` supports no cross-compilation. -After cloning TiLiA, install TiLiA's run and build dependencies with: +- After cloning TiLiA, install TiLiA's run and build dependencies with: ``` pip install -e . --group build ``` -To build a stand-alone executable, run the script: +- To build a stand-alone executable, run the script: ``` python scripts/deploy.py [ref_name] [os_type] ``` -(*n.b.: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.) +Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome. The executable will be found in the `build/[os_type]/exe` folder in the project directory. From 0d15c80ec9897f090e44f47be772e069b95616d3 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:19:12 +0100 Subject: [PATCH 58/75] docs: Fix typos, improve wording & formatting --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 20cf428c6..0594b12f1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ drawing

-TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured but easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI. +TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI. TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned. @@ -11,7 +11,7 @@ TiLiA allows users to annotate media files primarily through timelines of variou TiLiA desktop interface

-Here are some examples TiLiA visualizations: +Here are some examples of TiLiA visualizations: - Formal analysis of the Piano Sonata in D Major, K.284: - [First movement](https://tilia-app.com/viewer/135/) @@ -22,7 +22,7 @@ Here are some examples TiLiA visualizations: ## Current features - 7 kinds of timelines - AudioWave: visualize audio files through bars that represent changes in amplitude - - Beat: beat and measure markers with support to numbering + - Beat: beat and measure markers with support for numbering - Harmony: Roman numeral and chord symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords - Hierarchy: nested and levelled units organized in arbitrarily complex hierarchical structures - Marker: simple, labelled markers to indicate discrete events @@ -47,7 +47,7 @@ Tutorials on how to use TiLiA can be found on our [website](https://tilia-app.co ## Build or run from source -TiLiA can be also run and build from source. +TiLiA can also be run and built from source. ### Prerequisites @@ -79,8 +79,8 @@ git clone https://github.com/TimeLineAnnotator/desktop.git tilia-desktop ``` cd tilia-desktop ``` -Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps. -Failure to do so may cause issues with dependencies. +> Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps. +Failure to do so may cause dependency issues. - Install TiLiA and its dependencies with: ``` @@ -96,7 +96,7 @@ tilia ``` tilia --user-interface cli ``` -Note: The CLI is currently only available when run from source, and not in the compiled executable. +> Note: The CLI is currently only available when run from source, and not in the compiled executable. ### Building from source TiLiA uses [Nuitka](https://nuitka.net/) to build binaries. @@ -110,7 +110,7 @@ pip install -e . --group build ``` python scripts/deploy.py [ref_name] [os_type] ``` -Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome. +> Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome. The executable will be found in the `build/[os_type]/exe` folder in the project directory. From 7a4ad026943adf25160132f66ec9acc978ad07ed Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:28:08 +0000 Subject: [PATCH 59/75] fix: improve debug message --- tilia/__main__.py | 59 ++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/tilia/__main__.py b/tilia/__main__.py index 9b6c5dba8..e769ab26c 100644 --- a/tilia/__main__.py +++ b/tilia/__main__.py @@ -9,41 +9,52 @@ def deps_debug(exc: ImportError): + from tilia.constants import EMAIL, GITHUB_URL, WEBSITE_URL # noqa: E402 + + def _raise_deps_error(exc: ImportError, message: list[str]): + raise RuntimeError("\n".join(message)) from exc + + distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0] + link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux" root_path = Path( [*traceback.walk_tb(exc.__traceback__)][0][0].f_code.co_filename ).parent lib_path = root_path / "PySide6/qt-plugins/platforms/libqxcb.so" + if not lib_path.exists(): - print( - "Could not find libqxcb.so file.\nDumping all files in tree for debug...\n", - subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"), - ) - raise RuntimeError( - "Could not locate the necessary libraries to run TiLiA." - ) from exc - - _, result = subprocess.getstatusoutput(f"ldd {lib_path.as_posix()}") - if "=> not found" in result: + if "__compiled__" not in globals(): + msg = [ + "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.", + "Did you forget to install python dependencies?", + ] + else: + msg = [ + "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.", + f"Open an issue on our repo at <{GITHUB_URL}> or contact us at <{EMAIL}> for help.\n" + "Dumping all files in tree for debug...", + subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"), + ] + _raise_deps_error(exc, msg) + + deps = subprocess.getoutput(f"ldd {lib_path.as_posix()}") + if "=> not found" in deps: missing_deps = [] - for line in result.splitlines(): + for line in deps.splitlines(): if "=> not found" in line: dep = line.strip().rstrip(" => not found") missing_deps.append(dep) - from tilia.constants import WEBSITE_URL # noqa: E402 - - distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0] - link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux" if missing_deps: - print( - f"""TiLiA could not start due to missing system dependencies. -Visit <{link}> for help on installation. -Missing libraries: -{missing_deps}""" - ) - webbrowser.open(link) - - raise RuntimeError("Install the necessary dependencies then restart.") from exc + deps = f"Missing libraries:\n{missing_deps}" + + msg = [ + "TiLiA could not start due to missing system dependencies.", + f"Visit <{link}> for help on installation.", + "Install the necessary dependencies then restart.\n", + deps, + ] + webbrowser.open(link) + _raise_deps_error(exc, msg) def main(): From 04e2d28ee08bee19264c875e9dd6262db73c6ace Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:11:14 +0000 Subject: [PATCH 60/75] docs: some explanatory notes --- pyproject.toml | 2 +- tilia/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed647f979..07a377ff1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ build = [ "Nuitka >= 4.0", "ordered-set==4.1.0", "pyyaml", - "setuptools<82.0.0; python_version >= '3.12'", + "setuptools<82.0.0; python_version >= '3.12'", # setuptools>82.0.0 no longer provides pkg_resources "wheel==0.38.4", "zstandard==0.20.0" ] diff --git a/tilia/settings.py b/tilia/settings.py index ef597e3fb..386d22331 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -122,7 +122,7 @@ def _get(self, group_name: str, setting: str, in_default=True): key = self._get_key(group_name, setting, in_default) try: value = self._settings.value(key, None) - except EOFError: + except EOFError: # happens when the group in self._settings is not initiated, but setting a value solves this. value = None if not value or not isinstance( value, type(self.DEFAULT_SETTINGS[group_name][setting]) From e1d87c0076a24a6681e41530ffa10a5bc6ed80f8 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:11:29 +0000 Subject: [PATCH 61/75] fix: typo --- .github/workflows/run-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 983ada4f3..9f7d4dff2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -87,6 +87,6 @@ jobs: shell: bash run: | echo "Starting GUI..." - timeout 10 tilia-desktop || true + timeout 10 tilia || true echo "Starting CLI..." - timeout 10 tilia-desktop -i=cli|| true + timeout 10 tilia -i=cli|| true From 9b2a6bb0d06259688fffba48af7100251a613f47 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:41:48 +0000 Subject: [PATCH 62/75] ci: fix and standardise --- .github/workflows/run-tests.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9f7d4dff2..60b311784 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -38,7 +38,7 @@ jobs: python-version: '3.13' # no pyside support timeout-minutes: 30 env: - QT_SELECT: "qt6" + QT_QPA_PLATFORM: offscreen steps: - uses: actions/checkout@v3 @@ -47,12 +47,15 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Setup xvfb (Linux) + - name: Setup additional Linux dependencies if: runner.os == 'Linux' run: | - sudo apt-get update - sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 libxcb-shape0 libglib2.0-0 libgl1-mesa-dev libpulse-dev - sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev + chmod +x ./scripts/linux-setup.sh + ./scripts/linux-setup.sh + + - name: Install timeout + if: runner.os == 'macOS' + run: brew install coreutils - uses: actions/cache@v4 with: @@ -86,7 +89,8 @@ jobs: - name: Check Program shell: bash run: | + export QT_DEBUG_PLUGINS=1 echo "Starting GUI..." timeout 10 tilia || true echo "Starting CLI..." - timeout 10 tilia -i=cli|| true + timeout 10 tilia -i=cli || true From 5474b6c3723da083e159395fb6a5dc881b9e9bfc Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:41:49 +0000 Subject: [PATCH 63/75] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0594b12f1..105121ea5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI. -TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned. +TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are seven types of timelines, but many more are planned.

TiLiA desktop interface From a78cee15c1d3ad07a80f712a61a23ffd62428ab6 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:44:08 +0000 Subject: [PATCH 64/75] fix: correct location --- scripts/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy.py b/scripts/deploy.py index 8c21216cf..264aec7be 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -156,7 +156,7 @@ def _create_lib() -> Path: filter=lambda x, _: ( x if x.name.startswith(tilia) - or x.name.startswith("TiLiA.egg-info") + or x.name.startswith(f"{base}/TiLiA.egg-info") or x.name in ext_data else None ), From db963064401af005fadbb715d2e9b30357871833 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:46:30 +0000 Subject: [PATCH 65/75] ci: bump actions version Node.js 20 deprecation --- .github/workflows/build.yml | 10 +++++----- .github/workflows/run-tests.yml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 243afb478..dee15306c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,10 +28,10 @@ jobs: QT_QPA_PLATFORM: offscreen steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 cache: "pip" @@ -115,7 +115,7 @@ jobs: echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true - name: Upload executable - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.build-exe.outputs.out-filename }} path: build/*/exe @@ -132,10 +132,10 @@ jobs: QT_DEBUG_PLUGINS: 1 QT_QPA_PLATFORM: offscreen steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Download executable - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: build/ pattern: 'TiLiA-v*' diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 60b311784..1c95dcb4e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -41,9 +41,9 @@ jobs: QT_QPA_PLATFORM: offscreen steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -57,7 +57,7 @@ jobs: if: runner.os == 'macOS' run: brew install coreutils - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} From a5bcdd93152cdd6368a4788abf10b4f7acfcfb27 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:33:48 +0000 Subject: [PATCH 66/75] ci: don't zip mac wan't doing anything to improve the file size --- .github/workflows/build.yml | 3 --- scripts/deploy.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dee15306c..80de32e0e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,9 +77,6 @@ jobs: echo "\n\nStarting CLI..." echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true - zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true - rm -rf ${{ steps.build-exe.outputs.out-filepath }} - - name: Test executable [Windows] if: runner.os == 'Windows' shell: bash diff --git a/scripts/deploy.py b/scripts/deploy.py index 264aec7be..8943e5099 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -261,10 +261,6 @@ def build(): with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"out-filepath={out_filepath.as_posix()}\n") f.write(f"out-filename={out_filename}\n") - if "mac" in build_os: - f.write( - f"zip-filepath={outdir.as_posix()}/exe/{out_filename}.zip\n" - ) os.chdir(old_dir) dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var) except Exception as e: From 703a523f66971413cf668f3c3e71218340efb658 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:26:54 +0000 Subject: [PATCH 67/75] fi: correct filename --- scripts/deploy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/deploy.py b/scripts/deploy.py index 8943e5099..97202952d 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -255,9 +255,8 @@ def build(): _build_exe() if os.environ.get("GITHUB_OUTPUT"): if "mac" in build_os: - out_filepath = outdir / "exe" / "tilia.app" - else: - out_filepath = outdir / "exe" / out_filename + os.rename(outdir / "exe" / "tilia.app", outdir / "exe" / out_filename) + out_filepath = outdir / "exe" / out_filename with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"out-filepath={out_filepath.as_posix()}\n") f.write(f"out-filename={out_filename}\n") From 0d3dc9abb3da5a8e44dc9d149ad62175523f4a51 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:53:39 +0000 Subject: [PATCH 68/75] fix: correct filename --- scripts/deploy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/deploy.py b/scripts/deploy.py index 97202952d..0a2ecc456 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -255,7 +255,10 @@ def build(): _build_exe() if os.environ.get("GITHUB_OUTPUT"): if "mac" in build_os: - os.rename(outdir / "exe" / "tilia.app", outdir / "exe" / out_filename) + os.rename( + outdir / "exe" / "tilia.app", + outdir / "exe" / (out_filename + ".app"), + ) out_filepath = outdir / "exe" / out_filename with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"out-filepath={out_filepath.as_posix()}\n") From ef849442d108dc4cbea06338f5d9b553541006a9 Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:15:33 +0000 Subject: [PATCH 69/75] fix: correct filename --- scripts/deploy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/deploy.py b/scripts/deploy.py index 0a2ecc456..10b0461d3 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -259,7 +259,9 @@ def build(): outdir / "exe" / "tilia.app", outdir / "exe" / (out_filename + ".app"), ) - out_filepath = outdir / "exe" / out_filename + out_filepath = outdir / "exe" / (out_filename + ".app") + else: + out_filepath = outdir / "exe" / out_filename with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"out-filepath={out_filepath.as_posix()}\n") f.write(f"out-filename={out_filename}\n") From 82473ce66e10d519abfc17bb98dd13067a623852 Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 13:15:21 +0100 Subject: [PATCH 70/75] fix: handle tilia package metadata not found --- tests/test_constants.py | 24 ++++++++++++++++++++++++ tilia/constants.py | 25 +++++++++++++++---------- 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 tests/test_constants.py diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 000000000..ccee02720 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,24 @@ +import importlib.metadata +import sys +from unittest.mock import patch + + +def test_tilia_metadata_not_found(): + # If tilia.constants was already imported, we must remove it from sys.modules + # to ensure the module-level code runs again with our mocks. + if "tilia.constants" in sys.modules: + del sys.modules["tilia.constants"] + + # 1. Mock Path.exists to return False so it skips the pyproject.toml logic + with patch("pathlib.Path.exists", return_value=False), patch( + "importlib.metadata.metadata", + side_effect=importlib.metadata.PackageNotFoundError, + ): + + import tilia.constants as constants + + assert constants.APP_NAME == "" + assert constants.VERSION == "0.0.0" + assert constants.YEAR == "2022-2026" + assert constants.AUTHOR == "" + assert constants.EMAIL == "" diff --git a/tilia/constants.py b/tilia/constants.py index 6324dfc98..550b09ab4 100644 --- a/tilia/constants.py +++ b/tilia/constants.py @@ -16,16 +16,21 @@ EMAIL = setupcfg.get("authors", [{"email": ""}])[0]["email"] else: - setupcfg = metadata.metadata("TiLiA").json.copy() - - AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1) - EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1) - if "urls" not in setupcfg: - setupcfg["urls"] = {} - for url in setupcfg.get("project_url", {}): - k, _, v = url.partition(", ") - setupcfg["urls"][k] = v - setupcfg["description"] = setupcfg.get("summary", "") + try: + setupcfg = metadata.metadata("TiLiA").json.copy() + + AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1) + EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1) + if "urls" not in setupcfg: + setupcfg["urls"] = {} + for url in setupcfg.get("project_url", {}): + k, _, v = url.partition(", ") + setupcfg["urls"][k] = v + setupcfg["description"] = setupcfg.get("summary", "") + except metadata.PackageNotFoundError: + setupcfg = {} + AUTHOR = "" + EMAIL = "" APP_NAME = setupcfg.get("name", "") VERSION = setupcfg.get("version", "0.0.0") From e2bf10e31e0d5a9fe58c5be5881cf34472fb09a0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 16:23:55 +0100 Subject: [PATCH 71/75] fix: update event name in EXCLUDE_FROM_LOG event name --- .tilia.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tilia.env b/.tilia.env index e88594eb0..ffbaed9d8 100644 --- a/.tilia.env +++ b/.tilia.env @@ -1,3 +1,3 @@ LOG_REQUESTS = 1 -EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE +EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_STATE_RECORD ENVIRONMENT='dev' From 456c82fac46622455130d1b2447463d7af66424d Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 16:26:05 +0100 Subject: [PATCH 72/75] fix: circular import --- tilia/ui/timelines/collection/collection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index 05ae297f4..befb78670 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -41,8 +41,6 @@ from tilia.ui.timelines.view import TimelineView from .view import TimelineUIsView from ..base.element import TimelineUIElement -from ..beat import BeatTimelineUI -from ..harmony import HarmonyTimelineUI from ..selection_box import SelectionBoxQt from ..slider.timeline import SliderTimelineUI from ...dialogs.add_timeline_without_media import AddTimelineWithoutMedia @@ -916,6 +914,8 @@ def on_hierarchy_merge_split(self, new_units: list, old_units: list): self._update_loop_elements() def on_harmony_timeline_components_deserialized(self, id): + from ..harmony import HarmonyTimelineUI + timeline_ui = cast(HarmonyTimelineUI, self.get_timeline_ui(id)) timeline_ui.on_timeline_components_deserialized() From a334b63853e0b526279adebe776d01727e9b20fe Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 16:29:32 +0100 Subject: [PATCH 73/75] fix: use dict.get(key) instead of dict[key] --- tilia/requests/post.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tilia/requests/post.py b/tilia/requests/post.py index 9f78aba5f..bf70f9715 100644 --- a/tilia/requests/post.py +++ b/tilia/requests/post.py @@ -111,9 +111,7 @@ class Post(Enum): def _log_post(post, *args, **kwargs): - log_message = ( - f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners[post])}" - ) + log_message = f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners.get(post, ''))}" if post is Post.DISPLAY_ERROR: logger.warning(log_message) return From 55dbf3cb7f210c20e1182aa2a95ec43cce7369ed Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 16:30:13 +0100 Subject: [PATCH 74/75] test: fix error when tearing down ManageTimelines() --- tests/ui/windows/test_manage_timelines.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py index e0cec94b1..9604609f5 100644 --- a/tests/ui/windows/test_manage_timelines.py +++ b/tests/ui/windows/test_manage_timelines.py @@ -20,7 +20,9 @@ def assert_order_is_correct(tls: Timelines, expected: list[Timeline]): # assert list widget order for i, tl in enumerate(expected): tlui = get(Get.TIMELINE_UI, tl.id) - assert ManageTimelines().list_widget.item(i).timeline_ui == tlui + mt = ManageTimelines() + assert mt.list_widget.item(i).timeline_ui == tlui + mt.close() class TestChangeTimelineVisibility: From fdd65ff8918f8f528bb63f3ac6bb8f39edb500e2 Mon Sep 17 00:00:00 2001 From: Felipe Martins Date: Fri, 20 Mar 2026 16:50:45 +0100 Subject: [PATCH 75/75] test: timeline is deleted while ManagaTimelines is open --- tests/ui/windows/test_manage_timelines.py | 52 ++++++++++++++------- tilia/ui/timelines/collection/collection.py | 8 ++-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py index 9604609f5..3c246718c 100644 --- a/tests/ui/windows/test_manage_timelines.py +++ b/tests/ui/windows/test_manage_timelines.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from typing import Literal import pytest @@ -20,8 +21,17 @@ def assert_order_is_correct(tls: Timelines, expected: list[Timeline]): # assert list widget order for i, tl in enumerate(expected): tlui = get(Get.TIMELINE_UI, tl.id) - mt = ManageTimelines() - assert mt.list_widget.item(i).timeline_ui == tlui + with manage_timelines() as mt: + assert mt.list_widget.item(i).timeline_ui == tlui + + +@contextmanager +def manage_timelines(): + """Context manager for the ManageTimelines window.""" + mt = ManageTimelines() + try: + yield mt + finally: mt.close() @@ -29,10 +39,9 @@ class TestChangeTimelineVisibility: @staticmethod def toggle_timeline_is_visible(row: int = 0): """Toggles timeline visibility using the Manage Timelines window.""" - mt = ManageTimelines() - mt.list_widget.setCurrentRow(row) - QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton) - mt.close() + with manage_timelines() as mt: + mt.list_widget.setCurrentRow(row) + QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton) def test_hide(self, marker_tlui): commands.execute("timeline.set_is_visible", marker_tlui, True) @@ -66,17 +75,16 @@ def setup_timelines(self, tluis, tls): @staticmethod def click_set_ordinal_button(button: Literal["up", "down"], row: int): """Toggles timeline visibility using the ManageTimelines window.""" - mt = ManageTimelines() - mt.list_widget.setCurrentRow(row) - if button == "up": - button = mt.up_button - elif button == "down": - button = mt.down_button - else: - assert False, "Invalid button value." - - QTest.mouseClick(button, Qt.MouseButton.LeftButton) - mt.close() + with manage_timelines() as mt: + mt.list_widget.setCurrentRow(row) + if button == "up": + button = mt.up_button + elif button == "down": + button = mt.down_button + else: + assert False, "Invalid button value." + + QTest.mouseClick(button, Qt.MouseButton.LeftButton) def test_increase_ordinal(self, tls, setup_timelines): tl0, tl1, tl2 = setup_timelines @@ -143,3 +151,13 @@ def test_decrease_ordinal_with_last_selected_does_nothing( self.click_set_ordinal_button("down", 2) assert_order_is_correct(tls, [tl0, tl1, tl2]) + + +class TesttimelinesChangeWhileOpen: + def test_timeline_is_deleted(self, tluis): + commands.execute("timelines.add.marker", name="") + with manage_timelines() as mt: + mt.list_widget.setCurrentRow(0) + commands.execute("timeline.delete", tluis[0], confirm=False) + + # Much more could be tested here. diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py index befb78670..9dfa0a9ac 100644 --- a/tilia/ui/timelines/collection/collection.py +++ b/tilia/ui/timelines/collection/collection.py @@ -1132,14 +1132,12 @@ def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI): @staticmethod @command_callback - def on_timeline_delete(timeline_ui: TimelineUI): - confirmed = get( + def on_timeline_delete(timeline_ui: TimelineUI, confirm: bool = True) -> bool: + if confirm and not get( Get.FROM_USER_YES_OR_NO, "Delete timeline", "Are you sure you want to delete the selected timeline? This can be undone later.", - ) - - if not confirmed: + ): return False get(Get.TIMELINE_COLLECTION).delete_timeline(timeline_ui.timeline)