Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add testing support #88

Closed
wants to merge 10 commits into from
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ TagStudio is a photo & file organization application with an underlying system t
- Linux/macOS: `source .venv/bin/activate`

3. Install the required packages:
`pip install -r requirements.txt`

- required to run the app: `pip install -r requirements.txt`
- required to develop: `pip install -r requirements-dev.txt`

_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._

To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.

### Launching

> [!NOTE]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pytest==8.2.0
ruff==0.4.2
pre-commit==3.7.0
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ PySide6>=6.5.1.1,<=6.6.3.1
PySide6_Addons>=6.5.1.1,<=6.6.3.1
PySide6_Essentials>=6.5.1.1,<=6.6.3.1
typing_extensions>=3.10.0.0,<=4.11.0
ujson>=5.8.0,<=5.9.0
ujson>=5.8.0,<=5.9.0
41 changes: 41 additions & 0 deletions tagstudio/src/core/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
VERSION: str = '9.2.0' # Major.Minor.Patch
VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release

# The folder & file names where TagStudio keeps its data relative to a library.
TS_FOLDER_NAME: str = '.TagStudio'
BACKUP_FOLDER_NAME: str = 'backups'
COLLAGE_FOLDER_NAME: str = 'collages'
LIBRARY_FILENAME: str = 'ts_library.json'

# TODO: Turn this whitelist into a user-configurable blacklist.
IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large',
'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp',
'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2']
VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv',
'flv', 'gifv', 'm4p', 'm4v', '3gp']
AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac',
'alac', 'wma', 'ogg', 'aiff']
DOC_TYPES: list[str] = ['txt', 'rtf', 'md',
'doc', 'docx', 'pdf', 'tex', 'odt', 'pages']
PLAINTEXT_TYPES: list[str] = ['txt', 'md', 'css', 'html', 'xml', 'json', 'js',
'ts', 'ini', 'htm', 'csv', 'php', 'sh', 'bat']
SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods']
PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp']
ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z']
PROGRAM_TYPES: list[str] = ['exe', 'app']
SHORTCUT_TYPES: list[str] = ['lnk', 'desktop', 'url']

ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES + AUDIO_TYPES + \
DOC_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \
ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES

BOX_FIELDS = ['tag_box', 'text_box']
TEXT_FIELDS = ['text_line', 'text_box']
DATE_FIELDS = ['datetime']

TAG_COLORS = ['', 'black', 'dark gray', 'gray', 'light gray', 'white', 'light pink',
'pink', 'red', 'red orange', 'orange', 'yellow orange', 'yellow',
'lime', 'light green', 'mint', 'green','teal', 'cyan', 'light blue',
'blue', 'blue violet', 'violet', 'purple', 'lavender', 'berry',
'magenta', 'salmon', 'auburn', 'dark brown', 'brown', 'light brown',
'blonde', 'peach', 'warm gray', 'cool gray', 'olive']
38 changes: 18 additions & 20 deletions tagstudio/src/core/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import ujson

from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag
from src.core import ts_core
from src.core.constants import TS_FOLDER_NAME, BACKUP_FOLDER_NAME, COLLAGE_FOLDER_NAME, VERSION, TEXT_FIELDS
from src.core.utils.str import strip_punctuation
from src.core.utils.web import strip_web_protocol

Expand Down Expand Up @@ -154,8 +154,6 @@ def add_tag(self, library:'Library', tag_id:int, field_id:int, field_index:int=N
self.fields[field_index][field_id] = sorted(tags, key=lambda t: library.get_tag(t).display_name(library))

# logging.info(f'Tags: {self.fields[field_index][field_id]}')



class Tag:
"""A Library Tag Object. Referenced by ID."""
Expand Down Expand Up @@ -546,8 +544,8 @@ def create_library(self, path) -> int:
path = os.path.normpath(path).rstrip('\\')

# If '.TagStudio' is included in the path, trim the path up to it.
if ts_core.TS_FOLDER_NAME in path:
path = path.split(ts_core.TS_FOLDER_NAME)[0]
if TS_FOLDER_NAME in path:
path = path.split(TS_FOLDER_NAME)[0]

try:
self.clear_internal_vars()
Expand All @@ -565,11 +563,11 @@ def verify_ts_folders(self) -> None:
"""Verifies/creates folders required by TagStudio."""

full_ts_path = os.path.normpath(
f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}')
f'{self.library_dir}/{TS_FOLDER_NAME}')
full_backup_path = os.path.normpath(
f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}/{ts_core.BACKUP_FOLDER_NAME}')
f'{self.library_dir}/{TS_FOLDER_NAME}/{BACKUP_FOLDER_NAME}')
full_collage_path = os.path.normpath(
f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}/{ts_core.COLLAGE_FOLDER_NAME}')
f'{self.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}')

if not os.path.isdir(full_ts_path):
os.mkdir(full_ts_path)
Expand Down Expand Up @@ -606,13 +604,13 @@ def open_library(self, path: str) -> int:
path = os.path.normpath(path).rstrip('\\')

# If '.TagStudio' is included in the path, trim the path up to it.
if ts_core.TS_FOLDER_NAME in path:
path = path.split(ts_core.TS_FOLDER_NAME)[0]
if TS_FOLDER_NAME in path:
path = path.split(TS_FOLDER_NAME)[0]

if os.path.exists(os.path.normpath(f'{path}/{ts_core.TS_FOLDER_NAME}/ts_library.json')):
if os.path.exists(os.path.normpath(f'{path}/{TS_FOLDER_NAME}/ts_library.json')):

try:
with open(os.path.normpath(f'{path}/{ts_core.TS_FOLDER_NAME}/ts_library.json'), 'r', encoding='utf-8') as f:
with open(os.path.normpath(f'{path}/{TS_FOLDER_NAME}/ts_library.json'), 'r', encoding='utf-8') as f:
json_dump: JsonLibary = ujson.load(f)
self.library_dir = str(path)
self.verify_ts_folders()
Expand Down Expand Up @@ -788,9 +786,9 @@ def open_library(self, path: str) -> int:
if return_code == 1:

if not os.path.exists(os.path.normpath(
f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}')):
f'{self.library_dir}/{TS_FOLDER_NAME}')):
os.makedirs(os.path.normpath(
f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}'))
f'{self.library_dir}/{TS_FOLDER_NAME}'))

self._map_filenames_to_entry_ids()

Expand Down Expand Up @@ -832,7 +830,7 @@ def to_json(self):
Used in saving the library to disk.
"""

file_to_save: JsonLibary = {"ts-version": ts_core.VERSION,
file_to_save: JsonLibary = {"ts-version": VERSION,
"ignored_extensions": [],
"tags": [],
"collations": [],
Expand Down Expand Up @@ -869,7 +867,7 @@ def save_library_to_disk(self):

self.verify_ts_folders()

with open(os.path.normpath(f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}/{filename}'), 'w', encoding='utf-8') as outfile:
with open(os.path.normpath(f'{self.library_dir}/{TS_FOLDER_NAME}/{filename}'), 'w', encoding='utf-8') as outfile:
outfile.flush()
ujson.dump(self.to_json(), outfile, ensure_ascii=False, escape_forward_slashes=False)
# , indent=4 <-- How to prettyprint dump
Expand All @@ -886,7 +884,7 @@ def save_library_backup_to_disk(self) -> str:
filename = f'ts_library_backup_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.json'

self.verify_ts_folders()
with open(os.path.normpath(f'{self.library_dir}/{ts_core.TS_FOLDER_NAME}/{ts_core.BACKUP_FOLDER_NAME}/{filename}'), 'w', encoding='utf-8') as outfile:
with open(os.path.normpath(f'{self.library_dir}/{TS_FOLDER_NAME}/{BACKUP_FOLDER_NAME}/{filename}'), 'w', encoding='utf-8') as outfile:
outfile.flush()
ujson.dump(self.to_json(), outfile, ensure_ascii=False, escape_forward_slashes=False)
end_time = time.time()
Expand Down Expand Up @@ -932,11 +930,11 @@ def refresh_dir(self):
# Scans the directory for files, keeping track of:
# - Total file count
# - Files without library entries
# for type in ts_core.TYPES:
# for type in TYPES:
start_time = time.time()
for f in glob.glob(self.library_dir + "/**/*", recursive=True):
# p = Path(os.path.normpath(f))
if ('$RECYCLE.BIN' not in f and ts_core.TS_FOLDER_NAME not in f
if ('$RECYCLE.BIN' not in f and TS_FOLDER_NAME not in f
and 'tagstudio_thumbs' not in f and not os.path.isdir(f)):
if os.path.splitext(f)[1][1:].lower() not in self.ignored_extensions:
self.dir_file_count += 1
Expand Down Expand Up @@ -2038,7 +2036,7 @@ def add_field_to_entry(self, entry_id: int, field_id: int) -> None:
# entry = self.entries[entry_index]
entry = self.get_entry(entry_id)
field_type = self.get_field_obj(field_id)['type']
if field_type in ts_core.TEXT_FIELDS:
if field_type in TEXT_FIELDS:
entry.fields.append({int(field_id): ''})
elif field_type == 'tag_box':
entry.fields.append({int(field_id): []})
Expand Down
44 changes: 1 addition & 43 deletions tagstudio/src/core/ts_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,9 @@
import json
import os

from src.core.constants import TEXT_FIELDS, TS_FOLDER_NAME
from src.core.library import Entry, Library

VERSION: str = '9.2.0' # Major.Minor.Patch
VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release

# The folder & file names where TagStudio keeps its data relative to a library.
TS_FOLDER_NAME: str = '.TagStudio'
BACKUP_FOLDER_NAME: str = 'backups'
COLLAGE_FOLDER_NAME: str = 'collages'
LIBRARY_FILENAME: str = 'ts_library.json'

# TODO: Turn this whitelist into a user-configurable blacklist.
IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large',
'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp',
'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2']
VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv',
'flv', 'gifv', 'm4p', 'm4v', '3gp']
AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac',
'alac', 'wma', 'ogg', 'aiff']
DOC_TYPES: list[str] = ['txt', 'rtf', 'md',
'doc', 'docx', 'pdf', 'tex', 'odt', 'pages']
PLAINTEXT_TYPES: list[str] = ['txt', 'md', 'css', 'html', 'xml', 'json', 'js',
'ts', 'ini', 'htm', 'csv', 'php', 'sh', 'bat']
SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods']
PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp']
ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z']
PROGRAM_TYPES: list[str] = ['exe', 'app']
SHORTCUT_TYPES: list[str] = ['lnk', 'desktop', 'url']

ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES + AUDIO_TYPES + \
DOC_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \
ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES

BOX_FIELDS = ['tag_box', 'text_box']
TEXT_FIELDS = ['text_line', 'text_box']
DATE_FIELDS = ['datetime']

TAG_COLORS = ['', 'black', 'dark gray', 'gray', 'light gray', 'white', 'light pink',
'pink', 'red', 'red orange', 'orange', 'yellow orange', 'yellow',
'lime', 'light green', 'mint', 'green','teal', 'cyan', 'light blue',
'blue', 'blue violet', 'violet', 'purple', 'lavender', 'berry',
'magenta', 'salmon', 'auburn', 'dark brown', 'brown', 'light brown',
'blonde', 'peach', 'warm gray', 'cool gray', 'olive']


class TagStudioCore:
"""
Instantiate this to establish a TagStudio session.
Expand Down
15 changes: 9 additions & 6 deletions tagstudio/src/core/utils/str.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

def strip_punctuation(string: str) -> str:
def strip_punctuation(text: str) -> str:
"""Returns a given string stripped of all punctuation characters."""
return string.replace('(', '').replace(')', '').replace('[', '') \
.replace(']', '').replace('{', '').replace('}', '').replace("'", '') \
.replace('`', '').replace('’', '').replace('‘', '').replace('"', '') \
.replace('“', '').replace('”', '').replace('_', '').replace('-', '') \
.replace(' ', '').replace(' ', '')
punctuation = '{}[]()\'"`‘’“”-_  '
result = text

for p in punctuation:
result = result.replace(p, '')

return result

2 changes: 1 addition & 1 deletion tagstudio/src/qt/modals/build_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from src.core.library import Library, Tag
from src.core.palette import ColorType, get_tag_color
from src.core.ts_core import TAG_COLORS
from src.core.constants import TAG_COLORS
from src.qt.widgets import PanelWidget, PanelModal, TagWidget
from src.qt.modals import TagSearchPanel

Expand Down
4 changes: 2 additions & 2 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
from PySide6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QPushButton, QLineEdit, QScrollArea, QFileDialog,
QSplashScreen, QMenu)
from humanfriendly import format_timespan

from src.core.library import ItemType
from src.core.ts_core import (PLAINTEXT_TYPES, TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES,
from src.core.ts_core import TagStudioCore
from src.core.constants import (PLAINTEXT_TYPES, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES,
SHORTCUT_TYPES, PROGRAM_TYPES, ARCHIVE_TYPES, PRESENTATION_TYPES,
SPREADSHEET_TYPES, DOC_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES,
LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME,
Expand Down
2 changes: 1 addition & 1 deletion tagstudio/src/qt/widgets/collage_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer, QSettings

from src.core.library import Library
from src.core.ts_core import DOC_TYPES, VIDEO_TYPES, IMAGE_TYPES
from src.core.constants import DOC_TYPES, VIDEO_TYPES, IMAGE_TYPES


ERROR = f'[ERROR]'
Expand Down
2 changes: 1 addition & 1 deletion tagstudio/src/qt/widgets/item_thumb.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QBoxLayout, QCheckBox

from src.core.library import ItemType, Library
from src.core.ts_core import AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES
from src.core.constants import AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES
from src.qt.flowlayout import FlowWidget
from src.qt.helpers import FileOpenerHelper
from src.qt.widgets import ThumbRenderer, ThumbButton
Expand Down
2 changes: 1 addition & 1 deletion tagstudio/src/qt/widgets/preview_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from humanfriendly import format_size

from src.core.library import Entry, ItemType, Library
from src.core.ts_core import VIDEO_TYPES, IMAGE_TYPES
from src.core.constants import VIDEO_TYPES, IMAGE_TYPES
from src.qt.helpers import FileOpenerLabel, FileOpenerHelper, open_file
from src.qt.modals import AddFieldModal
from src.qt.widgets import (ThumbRenderer, FieldContainer, TagBoxWidget, TextWidget, PanelModal, EditTextBox,
Expand Down
2 changes: 1 addition & 1 deletion tagstudio/src/qt/widgets/thumb_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance, ImageOps
from PySide6.QtCore import QObject, Signal, QSize
from PySide6.QtGui import QPixmap
from src.core.ts_core import PLAINTEXT_TYPES, VIDEO_TYPES, IMAGE_TYPES
from src.core.constants import PLAINTEXT_TYPES, VIDEO_TYPES, IMAGE_TYPES


ERROR = f'[ERROR]'
Expand Down
17 changes: 8 additions & 9 deletions tagstudio/tests/core/test_tags.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from src.core.library import Tag
from tagstudio.src.core.library import Tag


class TestTags:
def test_construction(self):
tag = Tag(id=1, name='Tag Name', shorthand='TN', aliases=[
'First A', 'Second A'], subtags_ids=[2, 3, 4], color='')
assert (tag)
def test_construction():
tag = Tag(id=1, name='Tag Name', shorthand='TN', aliases=[
'First A', 'Second A'], subtags_ids=[2, 3, 4], color='')
assert tag

def test_empty_construction(self):
tag = Tag(id=1, name='', shorthand='', aliases=[], subtags_ids=[], color='')
assert (tag)
def test_empty_construction():
tag = Tag(id=1, name='', shorthand='', aliases=[], subtags_ids=[], color='')
assert tag
Empty file.
12 changes: 12 additions & 0 deletions tagstudio/tests/utils/test_str.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest

from src.core.utils.str import strip_punctuation

@pytest.mark.parametrize("text, expected", [
('{[(parenthesis)]}', 'parenthesis'),
('‘“`"\'quotes\'"`”’', 'quotes'),
('_-  spacers', 'spacers'),
('{}[]()\'"`‘’“”-  ', '')
])
def test_strip_punctuation(text, expected):
assert strip_punctuation(text) == expected