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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ options:
List all file-tags which are found in file names but are not part of .filetags
--tag-gardening This is for getting an overview on tags that might require to be renamed (typos, singular/plural, ...). See also http://www.webology.org/2008/v5n3/a58.html
--force-cv Only allow tags that are part of the controlled vocabulary (.filetags)
--allow-problematic-characters
Allow problematic characters (":") in user-entered tags
-v, --verbose Enable verbose mode
-q, --quiet Enable quiet mode
--version Display version and exit
Expand Down
83 changes: 74 additions & 9 deletions filetags/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def safe_import(library):
# unused: INVOCATION_TIME = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime())
FILENAME_TAG_SEPARATOR = ' -- '
BETWEEN_TAG_SEPARATOR = ' '
PROBLEMATIC_TAG_CHARACTERS = [':']
CONTROLLED_VOCABULARY_FILENAME = ".filetags"
HINT_FOR_BEING_IN_VOCABULARY_TEMPLATE = ' *'
TAGFILTER_DIRECTORY = os.path.join(os.path.expanduser("~"), ".filetags_tagfilter")
Expand Down Expand Up @@ -291,6 +292,11 @@ def safe_import(library):
dest="force_cv", action="store_true",
help="Only allow tags that are part of the controlled vocabulary (.filetags)")

parser.add_argument("--allow-problematic-characters",
dest="allow_problematic_characters", action="store_true",
help="Allow problematic characters (" + ", ".join('"' + char + '"' for char in PROBLEMATIC_TAG_CHARACTERS) +
") in user-entered tags")

parser.add_argument("-v", "--verbose",
dest="verbose", action="store_true",
help="Enable verbose mode")
Expand Down Expand Up @@ -335,8 +341,12 @@ class SimpleCompleter(object):
def __init__(self, options):
self.options = sorted(options)

# removing '-' as a delimiter character in order to be able to use '-tagname' for removing:
readline.set_completer_delims(readline.get_completer_delims().replace('-', ''))
# remove delimiter characters to improve completion
# remove '-' in order to be able to complete '-tagname' for removing
# remove '_' & ':' to be able to complete 'key_' into 'key_value{1,2,3}'
readline.set_completer_delims(
readline.get_completer_delims().replace('-', '').replace('_', '').replace(':', '')
)

return

Expand Down Expand Up @@ -414,6 +424,7 @@ def __init__(self, root, vocabulary, upto9_tags_for_shortcuts, tags_for_visual,
self.entered_tags = ""
self.cancelled = False
self.force_cv_enabled = options.force_cv and not options.remove and not options.tagfilter and not options.tagtrees
self.allow_problematic_characters = options.allow_problematic_characters
low_contrast_fg_color = self.get_soft_foreground(root, 0.6) ## better than hard-coded gray values that interfere with default color schema

# Label for instructions
Expand Down Expand Up @@ -622,13 +633,24 @@ def submit_tags(self):

self.entered_tags = self.entry.get().strip()
self.cancelled = False

tags_for_validation = extract_tags_from_argument(self.entered_tags)
invalid_tags = force_cv_validator(self.force_cv_enabled, tags_for_validation, self.vocabulary)
if invalid_tags:
error_msg = "Invalid tags: " + BETWEEN_TAG_SEPARATOR.join(invalid_tags)
similar_msg = build_similar_to_invalid_tags_message(invalid_tags, self.vocabulary)
cv_invalid_tags = force_cv_validator(self.force_cv_enabled, tags_for_validation, self.vocabulary)
character_invalid_tags = problematic_characters_validator(self.allow_problematic_characters, tags_for_validation)
if cv_invalid_tags or character_invalid_tags:
combined_invalid_tags = []
if cv_invalid_tags:
combined_invalid_tags.extend(cv_invalid_tags)
if character_invalid_tags:
combined_invalid_tags.extend(character_invalid_tags)
combined_invalid_tags = list(dict.fromkeys(combined_invalid_tags))

error_msg = "Invalid tags: " + BETWEEN_TAG_SEPARATOR.join(combined_invalid_tags)
similar_msg = build_similar_to_invalid_tags_message(combined_invalid_tags, self.vocabulary)
if similar_msg:
error_msg += "\n" + similar_msg
if character_invalid_tags:
error_msg += "\n" + build_problematic_characters_message()
self.error_label.config(text=error_msg)
return

Expand Down Expand Up @@ -1669,6 +1691,30 @@ def force_cv_validator(force_cv_enabled, tags_for_validation, vocabulary):
return invalid_tags
return None

def problematic_characters_validator(allow_problematic_characters, tags_for_validation):
if allow_problematic_characters or not tags_for_validation:
return None

invalid_tags = []
for tag in tags_for_validation:
if tag.startswith('-'):
continue
for char in PROBLEMATIC_TAG_CHARACTERS:
if char in tag:
invalid_tags.append(tag)
break

if invalid_tags:
return list(dict.fromkeys(invalid_tags))
return None

def build_problematic_characters_message():
if len(PROBLEMATIC_TAG_CHARACTERS) == 1:
characters = '"' + PROBLEMATIC_TAG_CHARACTERS[0] + '"'
return "Character " + characters + " can be problematic and is prohibited."
characters = ", ".join('"' + char + '"' for char in PROBLEMATIC_TAG_CHARACTERS)
return "Characters " + characters + " can be problematic and are prohibited."

def build_similar_to_invalid_tags_message(invalid_tags, vocabulary):
suggestions = []
for tag in invalid_tags:
Expand Down Expand Up @@ -2447,10 +2493,21 @@ def ask_for_tags(vocabulary, controlled_vocabulary, upto9_tags_for_shortcuts, ta
sys.stdout.flush()
sys.exit(0)

validation_error = force_cv_validator(force_cv_enabled, tags_from_userinput, controlled_vocabulary)
if validation_error:
cv_validation_error = force_cv_validator(force_cv_enabled, tags_from_userinput, controlled_vocabulary)
character_validation_error = problematic_characters_validator(options.allow_problematic_characters, tags_from_userinput)

if cv_validation_error or character_validation_error:
if not gui:
previous_error = validation_error
combined_errors = []
if cv_validation_error:
combined_errors.extend(cv_validation_error)
if character_validation_error:
combined_errors.extend(character_validation_error)
combined_errors = list(dict.fromkeys(combined_errors))

if character_validation_error and combined_errors:
combined_errors[-1] = combined_errors[-1] + ". " + build_problematic_characters_message()
previous_error = combined_errors
continue
previous_error = None

Expand Down Expand Up @@ -3213,6 +3270,14 @@ def main():
logging.info(similar_msg)
sys.exit(22)

if options.tags:
invalid_tags = problematic_characters_validator(options.allow_problematic_characters, tags_from_userinput)
if invalid_tags:
logging.error(colorama.Fore.RED + "Invalid tags: " +
colorama.Style.RESET_ALL + BETWEEN_TAG_SEPARATOR.join(invalid_tags))
logging.error(build_problematic_characters_message())
sys.exit(23)

if options.remove:
logging.info("removing tags \"%s\" ..." % str(BETWEEN_TAG_SEPARATOR.join(tags_from_userinput)))
elif options.tagfilter:
Expand Down
5 changes: 4 additions & 1 deletion tests/unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ def test_get_upto_nine_keys_of_dict_with_highest_value(self):
myresult)

def test_get_common_tags_from_files(self):

self.assertEqual(filetags.get_common_tags_from_files(['file1.txt']), [])
self.assertEqual(filetags.get_common_tags_from_files(['file1 -- foo.txt']), ['foo'])
self.assertSetEqual(set(filetags.get_common_tags_from_files(['file1 -- foo bar.txt'])), set(['foo', 'bar']))
Expand Down Expand Up @@ -214,6 +213,10 @@ def test_get_invalid_tags_for_vocabulary(self):
self.assertEqual(filetags.get_invalid_tags_for_vocabulary(['-foo', '-qux'], vocabulary), ['-qux'])
self.assertEqual(filetags.get_invalid_tags_for_vocabulary(['aa', 'aa', 'bb', 'aa'], ['bb']), ['aa'])

def test_problematic_characters_validator(self):
tags = ['foo', 'bar:baz']
self.assertEqual(filetags.problematic_characters_validator(False, tags), ['bar:baz'])

def test_build_similar_to_invalid_tags_message(self):
self.assertIsNone(filetags.build_similar_to_invalid_tags_message(['xxx'], ['foo', 'bar']))
self.assertEqual(filetags.build_similar_to_invalid_tags_message(['Simpson'], ['Simson', 'simpson']),
Expand Down
Loading