ChatMasterMind Application Refactor and Enhancement #8
130
chatmastermind/tags.py
Normal file
130
chatmastermind/tags.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""
|
||||
Module implementing tag related functions and classes.
|
||||
"""
|
||||
from typing import Type, TypeVar, Optional
|
||||
|
||||
TagInst = TypeVar('TagInst', bound='Tag')
|
||||
TagLineInst = TypeVar('TagLineInst', bound='TagLine')
|
||||
|
||||
|
||||
class TagError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Tag(str):
|
||||
"""
|
||||
A single tag. A string that can contain anything but the default separator (' ').
|
||||
"""
|
||||
# default separator
|
||||
default_separator = ' '
|
||||
# alternative separators (e. g. for backwards compatibility)
|
||||
alternative_separators = [',']
|
||||
|
||||
def __new__(cls: Type[TagInst], string: str) -> TagInst:
|
||||
"""
|
||||
Make sure the tag string does not contain the default separator.
|
||||
"""
|
||||
if cls.default_separator in string:
|
||||
raise TagError(f"Tag '{string}' contains the separator char '{cls.default_separator}'")
|
||||
instance = super().__new__(cls, string)
|
||||
return instance
|
||||
|
||||
|
||||
class TagLine(str):
|
||||
"""
|
||||
A line of tags. It starts with a prefix ('TAGS:'), followed by a list of tags,
|
||||
separated by the defaut separator (' '). Any operations on a TagLine will sort
|
||||
the tags.
|
||||
"""
|
||||
# the prefix
|
||||
prefix = 'TAGS:'
|
||||
|
||||
def __new__(cls: Type[TagLineInst], string: str) -> TagLineInst:
|
||||
"""
|
||||
Make sure the tagline string starts with the prefix.
|
||||
"""
|
||||
if not string.startswith(cls.prefix):
|
||||
raise TagError(f"TagLine '{string}' is missing prefix '{cls.prefix}'")
|
||||
instance = super().__new__(cls, string)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_set(cls: Type[TagLineInst], tags: set[Tag]) -> TagLineInst:
|
||||
"""
|
||||
Create a new TagLine from a set of tags.
|
||||
"""
|
||||
return cls(' '.join([TagLine.prefix] + sorted([t for t in tags])))
|
||||
|
||||
def tags(self) -> set[Tag]:
|
||||
"""
|
||||
Returns all tags contained in this line as a set.
|
||||
"""
|
||||
tagstr = self[len(self.prefix):].strip()
|
||||
separator = Tag.default_separator
|
||||
# look for alternative separators and use the first one found
|
||||
# -> we don't support different separators in the same TagLine
|
||||
for s in Tag.alternative_separators:
|
||||
if s in tagstr:
|
||||
separator = s
|
||||
break
|
||||
return set(sorted([Tag(t.strip()) for t in tagstr.split(separator)]))
|
||||
|
||||
def merge(self, taglines: set['TagLine']) -> 'TagLine':
|
||||
"""
|
||||
Merges the tags of all given taglines into the current one
|
||||
and returns a new TagLine.
|
||||
"""
|
||||
merged_tags = self.tags()
|
||||
for tl in taglines:
|
||||
merged_tags |= tl.tags()
|
||||
return self.from_set(set(sorted(merged_tags)))
|
||||
|
||||
def delete_tags(self, tags: set[Tag]) -> 'TagLine':
|
||||
"""
|
||||
Deletes the given tags and returns a new TagLine.
|
||||
"""
|
||||
return self.from_set(self.tags().difference(tags))
|
||||
|
||||
def add_tags(self, tags: set[Tag]) -> 'TagLine':
|
||||
"""
|
||||
Adds the given tags and returns a new TagLine.
|
||||
"""
|
||||
return self.from_set(set(sorted(self.tags() | tags)))
|
||||
|
||||
def rename_tags(self, tags: set[tuple[Tag, Tag]]) -> 'TagLine':
|
||||
"""
|
||||
Renames the given tags and returns a new TagLine. The first
|
||||
tuple element is the old name, the second one is the new name.
|
||||
"""
|
||||
new_tags = self.tags()
|
||||
for t in tags:
|
||||
if t[0] in new_tags:
|
||||
new_tags.remove(t[0])
|
||||
new_tags.add(t[1])
|
||||
return self.from_set(set(sorted(new_tags)))
|
||||
|
||||
def match_tags(self, tags_or: Optional[set[Tag]], tags_and: Optional[set[Tag]],
|
||||
tags_not: Optional[set[Tag]]) -> bool:
|
||||
"""
|
||||
Checks if the current TagLine matches the given tag requirements:
|
||||
- 'tags_or' : matches if this TagLine contains ANY of those tags
|
||||
- 'tags_and': matches if this TagLine contains ALL of those tags
|
||||
- 'tags_not': matches if this TagLine contains NONE of those tags
|
||||
|
||||
Note that it's sufficient if the TagLine matches one of 'tags_or' or 'tags_and',
|
||||
i. e. you can select a TagLine if it either contains one of the tags in 'tags_or'
|
||||
or all of the tags in 'tags_and' but it must never contain any of the tags in
|
||||
'tags_not'. If 'tags_or' and 'tags_and' are 'None', they match all tags (tag
|
||||
exclusion is still done if 'tags_not' is not 'None').
|
||||
"""
|
||||
tag_set = self.tags()
|
||||
required_tags_present = False
|
||||
excluded_tags_missing = False
|
||||
if ((tags_or is None and tags_and is None)
|
||||
or (tags_or and any(tag in tag_set for tag in tags_or)) # noqa: W503
|
||||
or (tags_and and all(tag in tag_set for tag in tags_and))): # noqa: W503
|
||||
required_tags_present = True
|
||||
if ((tags_not is None)
|
||||
or (not any(tag in tag_set for tag in tags_not))): # noqa: W503
|
||||
excluded_tags_missing = True
|
||||
return required_tags_present and excluded_tags_missing
|
||||
Loading…
x
Reference in New Issue
Block a user