mirror of
https://github.com/novoid/filetags.git
synced 2026-06-14 03:21:19 +00:00
Merge pull request #95 from nbehrnd/dev
resolve ci_pytest issues for Windows
This commit is contained in:
commit
8585c6f964
4 changed files with 110 additions and 49 deletions
5
.github/workflows/ci_pytest.yml
vendored
5
.github/workflows/ci_pytest.yml
vendored
|
|
@ -44,6 +44,5 @@ jobs:
|
|||
|
||||
- name: verbose pytest
|
||||
run: |
|
||||
cd tests
|
||||
python -m pytest unit_tests.py
|
||||
python -m pytest unit_tests.py -v
|
||||
pytest
|
||||
pytest -v
|
||||
|
|
|
|||
|
|
@ -38,14 +38,16 @@ def safe_import(library):
|
|||
"\".\nPlease install it, e.g., with \"sudo pip install " + library + "\".")
|
||||
sys.exit(2)
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse # for handling command line arguments
|
||||
import errno # for throwing FileNotFoundError
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import argparse # for handling command line arguments
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import logging
|
||||
import errno # for throwing FileNotFoundError
|
||||
try:
|
||||
import tkinter as tk ## for --gui
|
||||
from tkinter import ttk, font ## for --gui
|
||||
|
|
@ -375,28 +377,28 @@ class TagDialog:
|
|||
## 2025-09-02 and therefore might contain errors and mistakes.
|
||||
## This needs to be re-checked by somebody with Tkinter
|
||||
## knowledge.
|
||||
|
||||
|
||||
# if widget is None:
|
||||
# widget = tk._default_root
|
||||
if widget is None:
|
||||
raise ValueError("No widget specified and no default root exists.")
|
||||
|
||||
|
||||
# Get widget's foreground and background
|
||||
bg = widget.cget("bg")
|
||||
fg = widget.cget("fg") if "fg" in widget.keys() else "black"
|
||||
|
||||
|
||||
# Convert to RGB
|
||||
r1, g1, b1 = widget.winfo_rgb(fg)
|
||||
r2, g2, b2 = widget.winfo_rgb(bg)
|
||||
|
||||
|
||||
# Blend and reduce to 8-bit
|
||||
r = int(r1 * ratio + r2 * (1 - ratio)) >> 8
|
||||
g = int(g1 * ratio + g2 * (1 - ratio)) >> 8
|
||||
b = int(b1 * ratio + b2 * (1 - ratio)) >> 8
|
||||
|
||||
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
|
||||
|
||||
def __init__(self, root, vocabulary, upto9_tags_for_shortcuts, tags_for_visual, number_of_files, hint_str, tag_list):
|
||||
## Warning: this function was mostly programmed by ChatGPT
|
||||
## 2025-09-01 and therefore might contain errors and mistakes.
|
||||
|
|
@ -428,7 +430,7 @@ class TagDialog:
|
|||
self.label.pack(padx=(0,0), pady=(30,0))
|
||||
self.label = tk.Label(self.root, font="bold", text=existingtags)
|
||||
self.label.pack(pady=(0,30))
|
||||
|
||||
|
||||
self.label = tk.Label(self.root, fg=low_contrast_fg_color, text=hint_str)
|
||||
self.label.pack(pady=(0,0))
|
||||
self.label = tk.Label(self.root, text='\n'.join(tag_list))
|
||||
|
|
@ -484,15 +486,15 @@ class TagDialog:
|
|||
self.submit_button = tk.Button(self.root, text="Tag!", command=self.submit_tags)
|
||||
self.submit_button.pack(side=tk.RIGHT, padx=(20,30), pady=20)
|
||||
|
||||
|
||||
|
||||
def on_keyrelease(self, event):
|
||||
""" Handle key release to filter the word completions. """
|
||||
|
||||
|
||||
## Warning: this function was mostly programmed by ChatGPT
|
||||
## 2025-09-01 and therefore might contain errors and mistakes.
|
||||
## This needs to be re-checked by somebody with Tkinter
|
||||
## knowledge.
|
||||
|
||||
|
||||
user_input = self.entry.get().strip()
|
||||
|
||||
# Split the input into words
|
||||
|
|
@ -539,7 +541,7 @@ class TagDialog:
|
|||
if len(matching_tags) > 1:
|
||||
# Find the longest common prefix
|
||||
common_prefix = self.longest_common_prefix(matching_tags)
|
||||
|
||||
|
||||
# Update the entry field with the common prefix
|
||||
new_input = user_input[:len(user_input) - len(current_word)] + common_prefix
|
||||
self.entry.delete(0, tk.END)
|
||||
|
|
@ -596,7 +598,7 @@ class TagDialog:
|
|||
## 2025-09-01 and therefore might contain errors and mistakes.
|
||||
## This needs to be re-checked by somebody with Tkinter
|
||||
## knowledge.
|
||||
|
||||
|
||||
if not words:
|
||||
return ""
|
||||
|
||||
|
|
@ -610,7 +612,7 @@ class TagDialog:
|
|||
prefix = prefix[:-1]
|
||||
if not prefix:
|
||||
return ""
|
||||
|
||||
|
||||
return prefix
|
||||
|
||||
def submit_tags(self):
|
||||
|
|
@ -638,7 +640,7 @@ class TagDialog:
|
|||
def on_cancel(self):
|
||||
# Just close the dialog
|
||||
self.cancelled = True
|
||||
self.root.destroy()
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def contains_tag(filename, tagname=False):
|
||||
|
|
@ -1131,7 +1133,7 @@ def split_up_filename(filename, exception_on_file_not_found=False):
|
|||
"""
|
||||
|
||||
# logging.debug(f"split_up_filename: called with: {filename= } {exception_on_file_not_found= }")
|
||||
|
||||
|
||||
if not os.path.exists(filename):
|
||||
# This does make sense for splitting up filenames that are about to be created for example:
|
||||
if exception_on_file_not_found:
|
||||
|
|
@ -1303,7 +1305,7 @@ def create_link(source, destination):
|
|||
If the destination file exists, an error is shown unless the --overwrite
|
||||
option is used which results in deleting the old file and replacing with
|
||||
the new link.
|
||||
|
||||
|
||||
@param source: a file name of the source, an existing file
|
||||
@param destination: a file name for the link which is about to be created
|
||||
|
||||
|
|
@ -1319,7 +1321,7 @@ def create_link(source, destination):
|
|||
logging.debug('destination exists and overwrite flag is not set → report error to user')
|
||||
error_exit(21, 'Trying to create new link but found an old file with same name. ' +
|
||||
'If you want me to overwrite older files, use the "--overwrite" option. Culprit: ' + destination)
|
||||
|
||||
|
||||
if IS_WINDOWS:
|
||||
# do lnk-files instead of symlinks:
|
||||
shell = win32com.client.Dispatch('WScript.Shell')
|
||||
|
|
@ -2002,7 +2004,7 @@ def locate_file_in_cwd_and_parent_directories(startfile, filename):
|
|||
os.chdir(original_dir)
|
||||
return filename_to_look_for
|
||||
parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
|
||||
|
||||
|
||||
os.chdir(original_dir)
|
||||
logging.debug('locate_file_in_cwd_and_parent_directories: did NOT find \"%s\" in current directory or any parent directory' %
|
||||
filename)
|
||||
|
|
@ -2151,7 +2153,7 @@ def get_tag_shortcut_information(tag_list, tags_get_added=True, tags_get_linked=
|
|||
@param tags_get_added: True if tags get added, False otherwise
|
||||
@param return: -
|
||||
"""
|
||||
|
||||
|
||||
if tags_get_added:
|
||||
if len(tag_list) < 9:
|
||||
hint_str = "Previously used tags in this directory:"
|
||||
|
|
@ -2173,9 +2175,9 @@ def get_tag_shortcut_information(tag_list, tags_get_added=True, tags_get_linked=
|
|||
for tag in tag_list:
|
||||
list_of_tag_hints.append(tag + ' (' + str(count) + ')')
|
||||
count += 1
|
||||
|
||||
|
||||
return hint_str, list_of_tag_hints
|
||||
|
||||
|
||||
def print_tag_shortcut_with_numbers(hint_str, tag_list):
|
||||
"""A list of tags from the list are printed to stdout. Each tag
|
||||
gets a number associated which corresponds to the position in the
|
||||
|
|
@ -2314,7 +2316,7 @@ def ask_for_tags_text_version(vocabulary, upto9_tags_for_shortcuts, hint_str, ta
|
|||
@param upto9_tags_for_shortcuts: array of tags which can be used to generate number-shortcuts
|
||||
@param return: list of up to top nine keys according to the rank of their values
|
||||
"""
|
||||
|
||||
|
||||
completionhint = ''
|
||||
if vocabulary and len(vocabulary) > 0:
|
||||
|
||||
|
|
@ -2382,11 +2384,11 @@ def ask_for_tags_gui_version(vocabulary, upto9_tags_for_shortcuts, hint_str, tag
|
|||
@param upto9_tags_for_shortcuts: array of tags which can be used to generate number-shortcuts
|
||||
@param return: list of up to top nine keys according to the rank of their values
|
||||
"""
|
||||
|
||||
|
||||
completionhint = ''
|
||||
if vocabulary and len(vocabulary) > 0:
|
||||
assert(vocabulary.__class__ == list)
|
||||
|
||||
|
||||
number_of_files = len(options.files)
|
||||
number_of_files_str = str(number_of_files)
|
||||
logging.debug("len(files) [%s]" % number_of_files_str)
|
||||
|
|
@ -2527,6 +2529,7 @@ def assert_empty_tagfilter_directory(directory):
|
|||
if not options.dryrun:
|
||||
safe_import('shutil') # for removing directories with shutil.rmtree()
|
||||
shutil.rmtree(directory)
|
||||
force_rmtree(directory)
|
||||
logging.debug('re-creating tagfilter directory "%s" ...' % str(directory))
|
||||
os.makedirs(directory)
|
||||
|
||||
|
|
@ -2972,7 +2975,7 @@ def main():
|
|||
handle_logging()
|
||||
|
||||
logging.debug(f'{options=}')
|
||||
|
||||
|
||||
if options.verbose and options.quiet:
|
||||
error_exit(1, "Options \"--verbose\" and \"--quiet\" found. " +
|
||||
"This does not make any sense, you silly fool :-)")
|
||||
|
|
@ -2988,7 +2991,7 @@ def main():
|
|||
|
||||
if not options.interactive and options.gui:
|
||||
logging.warning('Found option "--gui" without option "--interactive". Will ignore that.')
|
||||
|
||||
|
||||
if options.list_tags_by_number and options.list_tags_by_alphabet:
|
||||
error_exit(6, "Please use only one list-by-option at once.")
|
||||
|
||||
|
|
@ -3284,6 +3287,29 @@ def main():
|
|||
successful_exit()
|
||||
|
||||
|
||||
def force_rmtree(path):
|
||||
"""Provide a rmtree compatible both for Linux/MacOS and Windows.
|
||||
|
||||
Previous definitions worked well for Linux/MacOS and their tests
|
||||
by `unit_tests.py` launched by `pytest` passed with GitHub's
|
||||
osrunners. However, the very same tests constantly failed with
|
||||
the osrunner of Windows. After discussion, this function was
|
||||
provided by Claude AI/Sonnet 4.6."""
|
||||
import stat, tempfile
|
||||
def _remove_readonly(func, fpath, _exc):
|
||||
os.chmod(fpath, stat.S_IWRITE)
|
||||
func(fpath)
|
||||
# Windows locks the CWD; move away before attempting deletion
|
||||
try:
|
||||
cwd = os.getcwd()
|
||||
if os.path.abspath(cwd).startswith(os.path.abspath(path)):
|
||||
os.chdir(tempfile.gettempdir())
|
||||
except Exception:
|
||||
pass
|
||||
if os.path.isdir(path):
|
||||
rmtree(path, onerror=_remove_readonly)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
cd $(dirname $0)/..
|
||||
PYTHONPATH=".:" tests/unit_tests.py --verbose
|
||||
PYTHONPATH=".:" tests/test_unit_tests.py --verbose
|
||||
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Time-stamp: <2023-08-28 18:16:44 vk>
|
||||
|
|
@ -5,16 +6,17 @@
|
|||
# invoke tests using following command line:
|
||||
# ~/src/vktag % PYTHONPATH="~/src/filetags:" tests/unit_tests.py --verbose
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import filetags
|
||||
import tempfile
|
||||
import os.path
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
import tempfile
|
||||
import time # for sleep()
|
||||
import unittest
|
||||
from shutil import rmtree
|
||||
|
||||
import filetags
|
||||
|
||||
# TEMPLATE for debugging:
|
||||
# try:
|
||||
|
|
@ -416,8 +418,8 @@ class TestLocateAndParseControlledVocabulary(unittest.TestCase):
|
|||
|
||||
# Note: cwd = subdir3
|
||||
# Let's create all missing files in all dirs:
|
||||
filetags.create_link(self.subdir2_file, self.subdir1_file) # create link
|
||||
filetags.create_link(self.subdir2b_file, self.tempdir_file) # create link
|
||||
filetags.create_link(self.subdir2_file, self.subdir1_file + ".lnk") # create link
|
||||
filetags.create_link(self.subdir2b_file, self.tempdir_file + ".lnk") # create link
|
||||
|
||||
# prio 1 = .filetag file in startfile directory
|
||||
self.assertEqual(filetags.locate_and_parse_controlled_vocabulary(self.subdir1_test_file),
|
||||
|
|
@ -865,7 +867,7 @@ class TestHierarchyWithFilesAndFolders(unittest.TestCase):
|
|||
filetags.handle_file(os.path.join(self.tempdir, 'sub dir 1', 'foo4 -- bar.txt'),
|
||||
['testtag'], do_remove=False, do_filter=False, dryrun=False)
|
||||
self.assertEqual(self.file_exists(os.path.join(self.tempdir, 'sub dir 1', 'foo4 -- bar testtag.txt')), True)
|
||||
|
||||
|
||||
def test_tagtrees_with_tagfilter_and_no_filtertag(self):
|
||||
|
||||
filetags.generate_tagtrees(directory=self.subdir2,
|
||||
|
|
@ -878,16 +880,34 @@ class TestHierarchyWithFilesAndFolders(unittest.TestCase):
|
|||
self.assertEqual(len(os.listdir(self.subdir2)), 5) # 5 entries in this directory
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'bar')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'bar'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'bar'))),
|
||||
set(['baz', 'foo1 -- bar.txt', 'foo2 -- bar baz.txt']))
|
||||
if sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'bar'))),
|
||||
set(['baz', 'foo1 -- bar.txt.lnk', 'foo2 -- bar baz.txt.lnk']))
|
||||
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'baz')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['bar', 'teststring1', 'foo2 -- bar baz.txt', 'foo3 -- baz teststring1.txt']))
|
||||
|
||||
if sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['bar', 'teststring1', 'foo2 -- bar baz.txt.lnk', 'foo3 -- baz teststring1.txt.lnk']))
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'teststring1')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
set(['baz', 'foo3 -- baz teststring1.txt']))
|
||||
if sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
set(['baz', 'foo3 -- baz teststring1.txt.lnk']))
|
||||
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'nontagged_items')))
|
||||
|
||||
|
|
@ -906,12 +926,23 @@ class TestHierarchyWithFilesAndFolders(unittest.TestCase):
|
|||
self.assertFalse(os.path.isdir(os.path.join(self.subdir2, 'bar')))
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'baz')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['teststring1', 'foo3 -- baz teststring1.txt']))
|
||||
elif sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['teststring1', 'foo3 -- baz teststring1.txt.lnk']))
|
||||
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'teststring1')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
set(['baz', 'foo3 -- baz teststring1.txt']))
|
||||
if sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'teststring1'))),
|
||||
set(['baz', 'foo3 -- baz teststring1.txt.lnk']))
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'nontagged_items')))
|
||||
|
||||
|
|
@ -931,8 +962,13 @@ class TestHierarchyWithFilesAndFolders(unittest.TestCase):
|
|||
self.assertFalse(os.path.isdir(os.path.join(self.subdir2, 'bar')))
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'baz')))
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['teststring1', 'foo3 -- baz teststring1.txt']))
|
||||
if sys.platform == "win32":
|
||||
self.assertEqual(set(os.listdir(os.path.join(self.subdir2, 'baz'))),
|
||||
set(['teststring1', 'foo3 -- baz teststring1.txt.lnk']))
|
||||
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.subdir2, 'teststring1')))
|
||||
|
||||
Loading…
Reference in a new issue