From 45b7ce8007230f09fe01326440973e612db57fb4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Sat, 16 Nov 2024 18:36:57 -0700 Subject: [PATCH] Args of AnsiStr now only contains 2 strings: one formatted and one unformatted --- src/ansi_string/ansi_string.py | 151 ++++--- tests/test_ansi_str.py | 754 +++++++++++++++++++++++++++++++++ 2 files changed, 825 insertions(+), 80 deletions(-) create mode 100755 tests/test_ansi_str.py diff --git a/src/ansi_string/ansi_string.py b/src/ansi_string/ansi_string.py index 79cb7f1..3bb40ce 100644 --- a/src/ansi_string/ansi_string.py +++ b/src/ansi_string/ansi_string.py @@ -202,12 +202,9 @@ def __init__( if isinstance(s, AnsiString): incoming_fmts = s._fmts super().__init__(s.data) - elif isinstance(s, AnsiStr): - incoming_fmts = s._ansi_string._fmts - super().__init__(s._ansi_string.data) elif isinstance(s, str): - super().__init__(s) - self.set_ansi_str(s) + super().__init__('') + self.set_ansi_str(str(s)) else: raise TypeError('Invalid type for s') @@ -284,7 +281,7 @@ def set_ansi_str(self, s:str) -> None: def simplify(self): '''Attempts to simplify formatting by re-parsing the ANSI formatting data''' - self.set_ansi_str(self.__str__()) + self.set_ansi_str(str(self)) def _shift_settings_idx(self, num:int, keep_origin:bool): ''' @@ -862,15 +859,12 @@ def __iadd__(self, value:Union[str,'AnsiString','AnsiStr']) -> 'AnsiString': value - the right-hand-side value as str or AnsiString Returns: self ''' + if isinstance(value, str): + value = AnsiString(value) + if isinstance(value, AnsiString): incoming_str = value.data incoming_fmts = value._fmts - elif isinstance(value, AnsiStr): - incoming_str = value._ansi_string.data - incoming_fmts = value._ansi_string._fmts - elif isinstance(value, str): - incoming_str = value - incoming_fmts = {} else: raise TypeError(f'value is invalid type: {type(value)}') @@ -925,12 +919,12 @@ def __eq__(self, value:'AnsiString') -> bool: def __contains__(self, value:Union[str,'AnsiString','AnsiStr',Any]) -> bool: ''' Returns True iff the str or the underlying str of an AnsiString is in this AnsiString ''' + if isinstance(value, str): + value = AnsiString(value) + if isinstance(value, AnsiString): return value.data in self.data - elif isinstance(value, AnsiStr): - return value._ansi_string.data in self.data - elif isinstance(value, str): - return value in self.data + return False def __len__(self) -> int: @@ -946,8 +940,6 @@ def join(*args:Union[str,'AnsiString','AnsiStr']) -> 'AnsiString': first_arg = args[0] if isinstance(first_arg, AnsiString): joint = first_arg.copy() - elif isinstance(first_arg, AnsiStr): - joint = first_arg._ansi_string.copy() elif isinstance(first_arg, str): joint = AnsiString(first_arg) else: @@ -1689,25 +1681,27 @@ def __new__( ansi_string = s elif isinstance(s, AnsiStr): if settings: - ansi_string = AnsiString(s._ansi_string, *settings) + ansi_string = AnsiString(s, *settings) else: - ansi_string = s._ansi_string + instance = super().__new__(cls, str(s)) + instance.data = s.data + return instance elif isinstance(s, str): ansi_string = AnsiString(s, *settings) else: raise TypeError('Invalid type for s') instance = super().__new__(cls, str(ansi_string)) - instance._ansi_string = ansi_string + instance.data = ansi_string.base_str return instance @property def base_str(self) -> str: ''' Returns the base string without any formatting set. ''' - return self._ansi_string.base_str + return self.data def __len__(self) -> int: ''' Returns the length of the underlying string ''' - return self._ansi_string.__len__() + return self.data.__len__() def __add__(self, value:Union[str,'AnsiString','AnsiStr']) -> 'AnsiStr': ''' @@ -1717,11 +1711,8 @@ def __add__(self, value:Union[str,'AnsiString','AnsiStr']) -> 'AnsiStr': value - the right-hand-side value as str or AnsiString Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() - if isinstance(value, AnsiStr): - cpy += value._ansi_string - else: - cpy += value + cpy = AnsiString(str(self)) + cpy += value return AnsiStr(cpy) def __iadd__(self, value:Union[str,'AnsiString','AnsiStr']) -> 'AnsiStr': @@ -1743,7 +1734,7 @@ def __format__(self, __format_spec:str) -> str: semicolons (;) ex: ">10:bold;red" to make output right justify with width of 10, bold and red formatting ''' - return self._ansi_string.__format__(__format_spec) + return AnsiString(str(self)).__format__(__format_spec) def __getitem__(self, val:Union[int, slice]) -> 'AnsiStr': ''' @@ -1755,11 +1746,11 @@ def __getitem__(self, val:Union[int, slice]) -> 'AnsiStr': Note: the new copy may contain some references to AnsiSettings in the origin. This is ok since AnsiSettings are not internally modified after creation. ''' - return AnsiStr(self._ansi_string.__getitem__(val)) + return AnsiStr(AnsiString(str(self)).__getitem__(val)) def simplify(self): '''Attempts to simplify formatting by re-parsing the ANSI formatting data''' - return AnsiStr(self.__str__()) + return AnsiStr(str(self)) def apply_formatting( self, @@ -1776,7 +1767,7 @@ def apply_formatting( group - match the group to set Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.apply_formatting(settings, start, end, topmost) return AnsiStr(cpy) @@ -1794,7 +1785,7 @@ def remove_formatting( end - The string index where the setting(s) should be removed Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.remove_formatting(settings, start, end) return AnsiStr(cpy) @@ -1812,7 +1803,7 @@ def apply_formatting_for_match( group - match the group to set Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.apply_formatting_for_match(settings, match_object, group) return AnsiStr(cpy) @@ -1834,7 +1825,7 @@ def format_matching( count - the number of matches to format or -1 to match all Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.format_matching(matchspec, *format, regex=regex, match_case=match_case, count=count) return AnsiStr(cpy) @@ -1856,7 +1847,7 @@ def unformat_matching( count - the number of matches to unformat or -1 to match all Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.unformat_matching(matchspec, *format, regex=regex, match_case=match_case, count=count) return AnsiStr(cpy) @@ -1869,7 +1860,7 @@ def capitalize(self) -> 'AnsiString': Return a capitalized version of the string. More specifically, make the first character have upper case and the rest lower case. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.capitalize(inplace=True) return AnsiStr(cpy) @@ -1877,7 +1868,7 @@ def casefold(self) -> 'AnsiString': ''' Return a version of the string suitable for caseless comparisons. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.casefold(inplace=True) return AnsiStr(cpy) @@ -1889,7 +1880,7 @@ def center(self, width:int, fillchar:str=' ') -> 'AnsiStr': fillchar - the character used to fill empty spaces Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.center(width, fillchar, inplace=True) return AnsiStr(cpy) @@ -1901,7 +1892,7 @@ def ljust(self, width:int, fillchar:str=' ') -> 'AnsiStr': fillchar - the character used to fill empty spaces Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.ljust(width, fillchar, inplace=True) return AnsiStr(cpy) @@ -1913,7 +1904,7 @@ def rjust(self, width:int, fillchar:str=' ') -> 'AnsiStr': fillchar - the character used to fill empty spaces Returns: a new AnsiStr ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.rjust(width, fillchar, inplace=True) return AnsiStr(cpy) @@ -1929,7 +1920,7 @@ def __eq__(self, value:'AnsiStr') -> bool: def __contains__(self, value:Union[str,'AnsiString','AnsiStr',Any]) -> bool: ''' Returns True iff the str or the underlying str of an AnsiString is in this AnsiString ''' - return self._ansi_string.__contains__(value) + return AnsiString(str(self)).__contains__(value) @staticmethod def join(*args:Union[str,'AnsiString','AnsiStr']) -> 'AnsiStr': @@ -1940,7 +1931,7 @@ def lower(self) -> 'AnsiStr': ''' Convert to lowercase into a new AnsiStr. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.lower(inplace=True) return AnsiStr(cpy) @@ -1948,7 +1939,7 @@ def upper(self) -> 'AnsiStr': ''' Convert to uppercase into a new AnsiStr. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.upper(inplace=True) return AnsiStr(cpy) @@ -1958,7 +1949,7 @@ def lstrip(self, chars:str=None) -> 'AnsiStr': Parameters: chars - If not None, remove characters in chars instead ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.lstrip(chars, inplace=True) return AnsiStr(cpy) @@ -1969,7 +1960,7 @@ def clip(self, start:int=None, end:int=None) -> 'AnsiStr': start - start index end - end index ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.clip(start, end, inplace=True) return AnsiStr(cpy) @@ -1981,7 +1972,7 @@ def rstrip(self, chars:str=None) -> 'AnsiStr': inplace - when True, do the conversion in-place and return self; when False, do the conversion on a copy and return the copy ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.rstrip(chars, inplace=True) return AnsiStr(cpy) @@ -1991,7 +1982,7 @@ def strip(self, chars:str=None) -> 'AnsiStr': Parameters: chars - If not None, remove characters in chars instead ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.strip(chars, inplace=True) return AnsiStr(cpy) @@ -2004,7 +1995,7 @@ def partition(self, sep:str) -> Tuple['AnsiStr','AnsiStr','AnsiStr']: If the separator is not found, returns a 3-tuple containing the original string and two empty strings. ''' - return [AnsiStr(x) for x in self._ansi_string.partition(sep)] + return [AnsiStr(x) for x in AnsiString(str(self)).partition(sep)] def rpartition(self, sep:str) -> Tuple['AnsiStr','AnsiStr','AnsiStr']: ''' @@ -2015,7 +2006,7 @@ def rpartition(self, sep:str) -> Tuple['AnsiStr','AnsiStr','AnsiStr']: If the separator is not found, returns a 3-tuple containing the original string and two empty strings. ''' - return [AnsiStr(x) for x in self._ansi_string.rpartition(sep)] + return [AnsiStr(x) for x in AnsiString(str(self)).rpartition(sep)] def ansi_settings_at(self, idx:int) -> List[AnsiSetting]: ''' @@ -2023,7 +2014,7 @@ def ansi_settings_at(self, idx:int) -> List[AnsiSetting]: Parameters: idx - the index to get settings of ''' - return self._ansi_string.ansi_settings_at(idx) + return AnsiString(str(self)).ansi_settings_at(idx) def settings_at(self, idx:int) -> str: ''' @@ -2031,7 +2022,7 @@ def settings_at(self, idx:int) -> str: Parameters: idx - the index to get settings of ''' - return self._ansi_string.settings_at(idx) + return AnsiString(str(self)).settings_at(idx) def removeprefix(self, prefix:str) -> 'AnsiStr': ''' @@ -2042,7 +2033,7 @@ def removeprefix(self, prefix:str) -> 'AnsiStr': Parameters: prefix - the prefix to remove ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.removeprefix(prefix, inplace=True) return AnsiStr(cpy) @@ -2056,7 +2047,7 @@ def removesuffix(self, suffix:str) -> 'AnsiString': Parameters: suffix - the suffix to remove ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.removesuffix(suffix, inplace=True) return AnsiStr(cpy) @@ -2070,7 +2061,7 @@ def replace(self, old:str, new:Union[str,'AnsiString'], count:int=-1) -> 'AnsiSt formatting of the first character of the old string count - the number of occurrences to replace or -1 to replace all occurrences ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.replace(old, new, count, inplace=True) return AnsiStr(cpy) @@ -2079,7 +2070,7 @@ def count(self, sub:str, start:int=None, end:int=None) -> int: Return the number of non-overlapping occurrences of substring sub in string S[start:end]. Optional arguments start and end are interpreted as in slice notation. ''' - return self._ansi_string.count(sub, start, end) + return self.data.count(sub, start, end) def encode(self, encoding:str="utf-8", errors:str="strict") -> bytes: ''' @@ -2092,14 +2083,14 @@ def encode(self, encoding:str="utf-8", errors:str="strict") -> bytes: a UnicodeEncodeError. Other possible values are 'ignore', 'replace' and 'xmlcharrefreplace' as well as any other name registered with codecs.register_error that can handle UnicodeEncodeErrors. ''' - return self._ansi_string.encode(encoding, errors) + return str(self).encode(encoding, errors) def endswith(self, suffix:str, start:int=None, end:int=None) -> bool: ''' Return True if S ends with the specified suffix, False otherwise. With optional start, test S beginning at that position. With optional end, stop comparing S at that position. suffix can also be a tuple of strings to try. ''' - return self._ansi_string.endswith(suffix, start, end) + return self.data.endswith(suffix, start, end) def expandtabs(self, tabsize:int=8) -> 'AnsiStr': ''' @@ -2116,7 +2107,7 @@ def find(self, sub:str, start:int=None, end:int=None) -> int: Return -1 on failure. ''' - return self._ansi_string.find(sub, start, end) + return self.data.find(sub, start, end) def index(self, sub:str, start:int=None, end:int=None) -> int: ''' @@ -2125,7 +2116,7 @@ def index(self, sub:str, start:int=None, end:int=None) -> int: Raises ValueError when the substring is not found. ''' - return self._ansi_string.index(sub, start, end) + return self.data.index(sub, start, end) def isalnum(self) -> bool: ''' @@ -2134,7 +2125,7 @@ def isalnum(self) -> bool: A string is alpha-numeric if all characters in the string are alpha-numeric and there is at least one character in the string ''' - return self._ansi_string.isalnum() + return self.data.isalnum() def isalpha(self) -> bool: ''' @@ -2143,7 +2134,7 @@ def isalpha(self) -> bool: A string is alphabetic if all characters in the string are alphabetic and there is at least one character in the string. ''' - return self._ansi_string.isalpha() + return self.data.isalpha() def isascii(self) -> bool: ''' @@ -2153,7 +2144,7 @@ def isascii(self) -> bool: ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. ''' - return self._ansi_string.isascii() + return self.data.isascii() def isdecimal(self) -> bool: ''' @@ -2162,7 +2153,7 @@ def isdecimal(self) -> bool: A string is a decimal string if all characters in the string are decimal and there is at least one character in the string. ''' - return self._ansi_string.isdecimal() + return self.data.isdecimal() def isdigit(self) -> bool: ''' @@ -2171,7 +2162,7 @@ def isdigit(self) -> bool: A string is a digit string if all characters in the string are digits and there is at least one character in the string. ''' - return self._ansi_string.isdigit() + return self.data.isdigit() def isidentifier(self) -> bool: ''' @@ -2179,7 +2170,7 @@ def isidentifier(self) -> bool: Call keyword.iskeyword(s) to test whether string s is a reserved identifier, such as "def" or "class". ''' - return self._ansi_string.isidentifier() + return self.data.isidentifier() def islower(self) -> bool: ''' @@ -2188,7 +2179,7 @@ def islower(self) -> bool: A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. ''' - return self._ansi_string.islower() + return self.data.islower() def isnumeric(self) -> bool: ''' @@ -2197,7 +2188,7 @@ def isnumeric(self) -> bool: A string is numeric if all characters in the string are numeric and there is at least one character in the string. ''' - return self._ansi_string.isnumeric() + return self.data.isnumeric() def isprintable(self) -> bool: ''' @@ -2205,7 +2196,7 @@ def isprintable(self) -> bool: A string is printable if all of its characters are considered printable in repr() or if it is empty. ''' - return self._ansi_string.isprintable() + return self.data.isprintable() def isspace(self) -> bool: ''' @@ -2213,7 +2204,7 @@ def isspace(self) -> bool: A string is whitespace if all characters in the string are whitespace and there is at least one character in the string. ''' - return self._ansi_string.isspace() + return self.data.isspace() def istitle(self) -> bool: ''' @@ -2222,7 +2213,7 @@ def istitle(self) -> bool: In a title-cased string, upper- and title-case characters may only follow uncased characters and lowercase characters only cased ones. ''' - return self._ansi_string.istitle() + return self.data.istitle() def isupper(self) -> bool: ''' @@ -2231,7 +2222,7 @@ def isupper(self) -> bool: A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. ''' - return self._ansi_string.isupper() + return self.data.isupper() def rfind(self, sub:str, start:int=None, end:int=None) -> int: ''' @@ -2243,7 +2234,7 @@ def rfind(self, sub:str, start:int=None, end:int=None) -> int: Return -1 on failure. ''' - return self._ansi_string.rfind(sub, start, end) + return self.data.rfind(sub, start, end) def rindex(self, sub:str, start:int=None, end:int=None) -> int: ''' @@ -2255,7 +2246,7 @@ def rindex(self, sub:str, start:int=None, end:int=None) -> int: Raises ValueError when the substring is not found. ''' - return self._ansi_string.rindex(sub, start, end) + return self.data.rindex(sub, start, end) def split(self, sep:Union[str,None]=None, maxsplit:int=-1) -> List['AnsiStr']: ''' @@ -2272,7 +2263,7 @@ def split(self, sep:Union[str,None]=None, maxsplit:int=-1) -> List['AnsiStr']: Note, str.split() is mainly useful for data that has been intentionally delimited. With natural text that includes punctuation, consider using the regular expression module. ''' - return [AnsiStr(x) for x in self._ansi_string.split(sep, maxsplit)] + return [AnsiStr(x) for x in AnsiString(str(self)).split(sep, maxsplit)] def rsplit(self, sep:Union[str,None]=None, maxsplit:int=-1) -> List['AnsiStr']: ''' @@ -2288,7 +2279,7 @@ def rsplit(self, sep:Union[str,None]=None, maxsplit:int=-1) -> List['AnsiStr']: Splitting starts at the end of the string and works to the front. ''' - return [AnsiStr(x) for x in self._ansi_string.rsplit(sep, maxsplit)] + return [AnsiStr(x) for x in AnsiString(str(self)).rsplit(sep, maxsplit)] def splitlines(self, keepends:bool=False) -> List['AnsiStr']: ''' @@ -2296,11 +2287,11 @@ def splitlines(self, keepends:bool=False) -> List['AnsiStr']: Line breaks are not included in the resulting list unless keepends is given and true. ''' - return [AnsiStr(x) for x in self._ansi_string.splitlines(keepends)] + return [AnsiStr(x) for x in AnsiString(str(self)).splitlines(keepends)] def swapcase(self) -> 'AnsiStr': ''' Convert uppercase characters to lowercase and lowercase characters to uppercase. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.swapcase(inplace=True) return AnsiStr(cpy) @@ -2310,7 +2301,7 @@ def title(self) -> 'AnsiStr': More specifically, words start with uppercased characters and all remaining cased characters have lower case. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.title(inplace=True) return AnsiStr(cpy) @@ -2320,7 +2311,7 @@ def zfill(self, width:int) -> 'AnsiString': The string is never truncated. ''' - cpy = self._ansi_string.copy() + cpy = AnsiString(str(self)) cpy.zfill(width, inplace=True) return AnsiStr(cpy) diff --git a/tests/test_ansi_str.py b/tests/test_ansi_str.py new file mode 100755 index 0000000..e83ba48 --- /dev/null +++ b/tests/test_ansi_str.py @@ -0,0 +1,754 @@ +#!/usr/bin/env python3 + +import os +import sys +import unittest +from io import BytesIO, StringIO +from unittest.mock import patch + +THIS_FILE_PATH = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) +PROJECT_DIR = os.path.abspath(os.path.join(THIS_FILE_PATH, '..')) +SOURCE_DIR = os.path.abspath(os.path.join(PROJECT_DIR, 'src')) + +if os.path.isdir(SOURCE_DIR): + sys.path.insert(0, SOURCE_DIR) +from ansi_string import en_tty_ansi, AnsiFormat, AnsiString, AnsiStr, ColorComponentType, ColourComponentType + +def _is_windows(): + return sys.platform.lower().startswith('win') + +class FakeStdOut: + def __init__(self) -> None: + self.buffer = BytesIO() + +class FakeStdIn: + def __init__(self, loaded_str): + if isinstance(loaded_str, str): + loaded_str = loaded_str.encode() + self.buffer = BytesIO(loaded_str) + +class AnsiStrTests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + AnsiString.WITH_ASSERTIONS = True + + def test_verify_assertions_enabled(self): + # Sanity check + self.assertTrue(AnsiString.WITH_ASSERTIONS) + + def test_en_tty_ansi(self): + # Not a very useful test + en_tty_ansi() + + def test_no_format(self): + s = AnsiStr('No format') + self.assertEqual(str(s), 'No format') + + def test_from_ansi_string(self): + s = AnsiStr('\x1b[32mabc\x1b[m') + self.assertEqual(str(s), '\x1b[32mabc\x1b[m') + self.assertEqual(s.base_str, 'abc') + + def test_using_AnsiFormat(self): + s = AnsiStr('This is bold', AnsiFormat.BOLD) + self.assertEqual(str(s), '\x1b[1mThis is bold\x1b[m') + + def test_using_list_of_AnsiFormat(self): + s = AnsiStr('This is bold and red', [AnsiFormat.BOLD, AnsiFormat.RED]) + self.assertEqual(str(s), '\x1b[1;31mThis is bold and red\x1b[m') + + def test_using_list_of_various(self): + s = AnsiStr('Lots of formatting!', ['[1', AnsiFormat.UL_RED, 'rgb(0x12A03F);bg_white']) + self.assertEqual(str(s), '\x1b[1;4;58;5;9;38;2;18;160;63;47mLots of formatting!\x1b[m') + + def test_custom_formatting(self): + s = AnsiStr('This string contains custom formatting', '[38;2;175;95;95') + self.assertEqual(str(s), '\x1b[38;2;175;95;95mThis string contains custom formatting\x1b[m') + + def test_ranges(self): + s = AnsiStr('This string contains multiple color settings across different ranges') + s = s.apply_formatting(AnsiFormat.BOLD, 5, 11) + s = s.apply_formatting(AnsiFormat.BG_BLUE, 21, 29) + s = s.apply_formatting([AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC], 21, 35) + self.assertEqual( + str(s), + 'This \x1b[1mstring\x1b[m contains \x1b[44;38;5;214;3mmultiple\x1b[0;38;5;214;3m color\x1b[m settings ' + 'across different ranges' + ) + + def test_format_right_only(self): + s = AnsiStr('This has no ANSI formatting') + self.assertEqual( + f'{s:#>90}', + '###############################################################This has no ANSI formatting' + ) + + def test_format_right_justify_and_int(self): + s = AnsiStr('This string will be formatted bold and red, right justify') + self.assertEqual( + f'{s:>90:01;31}', + '\x1b[1;31m This string will be formatted bold and red, right justify\x1b[m' + ) + + def test_format_left_justify_and_strings(self): + s = AnsiStr('This string will be formatted bold and red', 'bold') + self.assertEqual( + '{:+<90:fg_red}'.format(s), + '\x1b[1;31mThis string will be formatted bold and red++++++++++++++++++++++++++++++++++++++++++++++++\x1b[m' + ) + + def test_format_center_and_verbatim_string(self): + s = AnsiStr('This string will be formatted bold and red') + self.assertEqual( + '{:*^90:[this is not parsed}'.format(s), + '\x1b[this is not parsedm************************This string will be formatted bold and red************************\x1b[m' + ) + + def test_no_format_and_rgb_functions(self): + s = AnsiStr('Manually adjust colors of foreground, background, and underline') + self.assertEqual( + f'{s::rgb(0x8A2BE2);bg_rgb(100, 232, 170);ul_rgb(0xFF, 0x63, 0x47)}', + '\x1b[38;2;138;43;226;48;2;100;232;170;4;58;2;255;99;71mManually adjust colors of foreground, background, and underline\x1b[m' + ) + + def test_no_format_and_rgb_functions2(self): + s = AnsiStr('Manually adjust colors of foreground, background, and underline') + fg_color = 0x8A2BE2 + bg_colors = (100, 232, 170) + ul_colors = [0xFF, 0x63, 0x47] + self.assertEqual( + f'{s::rgb({fg_color});bg_rgb({bg_colors});ul_rgb({ul_colors})}', + '\x1b[38;2;138;43;226;48;2;100;232;170;4;58;2;255;99;71mManually adjust colors of foreground, background, and underline\x1b[m' + ) + + def test_add(self): + s = AnsiStr('bold', 'bold') + AnsiStr('red', 'red') + self.assertEqual( + str(s), + '\x1b[1mbold\x1b[0;31mred\x1b[m' + ) + + def test_add_ansistring(self): + s = AnsiStr('bold', 'bold') + AnsiString('red', 'red') + self.assertEqual( + str(s), + '\x1b[1mbold\x1b[0;31mred\x1b[m' + ) + + def test_iadd(self): + s = AnsiStr('part bold') + s = s.apply_formatting(AnsiFormat.BOLD, 0, 3) + s += AnsiStr('red', 'red') + self.assertEqual( + str(s), + '\x1b[1mpar\x1b[mt bold\x1b[31mred\x1b[m' + ) + + def test_iadd_ansistring(self): + s = AnsiStr('part bold') + s = s.apply_formatting(AnsiFormat.BOLD, 0, 3) + s += AnsiString('red', 'red') + self.assertEqual( + str(s), + '\x1b[1mpar\x1b[mt bold\x1b[31mred\x1b[m' + ) + + def test_eq(self): + s=AnsiStr('red', 'red') + self.assertEqual(s, AnsiStr('red', AnsiFormat.FG_RED)) + + def test_neq(self): + s=AnsiStr('red', 'red') + self.assertNotEqual(s, AnsiStr('red', AnsiFormat.BG_RED)) + + def test_center(self): + s = AnsiStr('This string will be formatted bold and red', 'bold;red') + s2 = s.center(90, 'X') + self.assertEqual( + str(s2), + '\x1b[1;31mXXXXXXXXXXXXXXXXXXXXXXXXThis string will be formatted bold and redXXXXXXXXXXXXXXXXXXXXXXXX\x1b[m' + ) + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31mThis string will be formatted bold and red\x1b[m' + ) + + def test_ljust(self): + s = AnsiStr('This string will be formatted bold and red', 'bold;red') + s2 = s.ljust(90, 'X') + self.assertEqual( + str(s2), + '\x1b[1;31mThis string will be formatted bold and redXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\x1b[m' + ) + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31mThis string will be formatted bold and red\x1b[m' + ) + + def test_rjust(self): + s = AnsiStr('This string will be formatted bold and red', 'bold;red') + s2 = s.rjust(90, '0') + self.assertEqual( + str(s2), + '\x1b[1;31m000000000000000000000000000000000000000000000000This string will be formatted bold and red\x1b[m' + ) + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31mThis string will be formatted bold and red\x1b[m' + ) + + def test_strip(self): + s = AnsiStr(' T\t\r\n \v\f', 'bold;red') + s2 = s.strip() + self.assertEqual(str(s2), '\x1b[1;31mT\x1b[m') + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31m T\t\r\n \x0b\x0c\x1b[m' + ) + + def test_strip_all(self): + s = AnsiStr(' ', 'bold;red') + s2 = s.strip() + self.assertEqual(str(s2), '') + + def test_strip_no_right(self): + s = AnsiStr(' b', 'bold;red') + s2 = s.strip() + self.assertEqual(str(s2), '\x1b[1;31mb\x1b[m') + + def test_strip_no_change(self): + s = AnsiStr('b', 'bold;red') + s2 = s.strip() + self.assertEqual(str(s2), '\x1b[1;31mb\x1b[m') + + def test_lstrip(self): + s = AnsiStr(' T\t\r\n \v\f', 'bold;red') + s2 = s.lstrip() + self.assertEqual(str(s2), '\x1b[1;31mT\t\r\n \v\f\x1b[m') + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31m T\t\r\n \x0b\x0c\x1b[m' + ) + + def test_lstrip_all(self): + s = AnsiStr(' \t ', 'bold;red') + s2 = s.lstrip() + self.assertEqual(str(s2), '') + + def test_rstrip(self): + s = AnsiStr(' T\t\r\n \v\f', 'bold;red') + s2 = s.rstrip() + self.assertEqual(str(s2), '\x1b[1;31m T\x1b[m') + self.assertIsNot(s, s2) + self.assertEqual( + str(s), + '\x1b[1;31m T\t\r\n \x0b\x0c\x1b[m' + ) + + def test_rstrip_all(self): + s = AnsiStr(' \n ', 'bold;red') + s2 = s.rstrip() + self.assertEqual(str(s2), '') + + def test_partition_found(self): + s = AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s_orig = s + out = s.partition('will') + self.assertEqual( + [str(s) for s in out], + ['\x1b[38;5;90;3mThis string \x1b[m', '\x1b[38;5;90;3mwill\x1b[m', '\x1b[38;5;90;3m be formatted italic and purple\x1b[m'] + ) + self.assertIs(s, s_orig) + + def test_partition_not_found(self): + s = AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s_orig = s + out = s.partition('bella') + self.assertEqual( + [str(s) for s in out], + ['\x1b[38;5;90;3mThis string will be formatted italic and purple\x1b[m', '', ''] + ) + self.assertIs(s, s_orig) + + def test_rpartition_found(self): + s = AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s_orig = s + out = s.rpartition('l') + self.assertEqual( + [str(s) for s in out], + ['\x1b[38;5;90;3mThis string will be formatted italic and purp\x1b[m', '\x1b[38;5;90;3ml\x1b[m', '\x1b[38;5;90;3me\x1b[m'] + ) + self.assertIs(s, s_orig) + + def test_rpartition_not_found(self): + s = AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s_orig = s + out = s.rpartition('x') + self.assertEqual( + [str(s) for s in out], + ['\x1b[38;5;90;3mThis string will be formatted italic and purple\x1b[m', '', ''] + ) + self.assertIs(s, s_orig) + + + def test_get_item_edge_case(self): + # There used to be a bug where if a single character was retrieved right before the index where a new format was + # applied, it add a remove setting for something that didn't exist yet + s=AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s = s.apply_formatting('bold', 5, 11) + self.assertEqual(str(s[4]), '\x1b[38;5;90;3m \x1b[m') + self.assertEqual(str(s[5]), '\x1b[38;5;90;3;1ms\x1b[m') + + def test_settings_at(self): + s=AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s = s.apply_formatting('bold', 5, 11) + self.assertEqual(s.settings_at(4), '38;5;90;3') + self.assertEqual(s.settings_at(5), '38;5;90;3;1') + + def test_settings_at_no_format(self): + s=AnsiStr('dsfoi sdfsdfksjdbf') + s = s.apply_formatting('red', 4) + self.assertEqual(s.settings_at(3), '') + + def test_settings_at_out_of_range_high(self): + s=AnsiStr('aradfghsdfgsdfgdsfgdf', 'red') + self.assertEqual(s.settings_at(21), '') + + def test_settings_at_out_of_range_low(self): + s=AnsiStr('gvcsxghfwraedtygxc', 'orange') + self.assertEqual(s.settings_at(-1), '') + + def test_iterate(self): + s = AnsiStr('one ', 'bg_yellow') + AnsiStr('two ', AnsiFormat.UNDERLINE) + AnsiStr('three', '1') + s2 = AnsiStr() + # Recreate the original string by iterating the characters + for c in s: + s2 += c + # This will look the same, even though each character now has formatting + self.assertEqual(str(s2), str(s)) + self.assertIsNot(s, s2) + + def test_remove_prefix_not_found(self): + s = AnsiStr('blah blah', AnsiFormat.ALT_FONT_4) + s2 = s.removeprefix('nah') + self.assertEqual(str(s), '\x1b[14mblah blah\x1b[m') + self.assertIsNot(s, s2) + + def test_remove_suffix_not_found(self): + s = AnsiStr('blah blah', AnsiFormat.ALT_FONT_4) + s2 = s.removesuffix('nah') + self.assertEqual(str(s), '\x1b[14mblah blah\x1b[m') + self.assertIsNot(s, s2) + + def test_cat_edge_case(self): + a = AnsiStr('a', 'red') + b = AnsiStr('b', 'red') + # Two string with same formatting should merge formatting properly + c = a + b + self.assertEqual(str(c), '\x1b[31mab\x1b[m') + + def test_cat_edge_case2(self): + # The beginning of the RHS string contains the same formatting of the LHS string, but ends before last char + a = AnsiStr('abc', 'red', 'bold') + b = AnsiStr('xyz') + b = b.apply_formatting('red', end=-2) + b = b.apply_formatting('bold', end=-1) + c = a + b + self.assertEqual(str(c), '\x1b[31;1mabcx\x1b[0;1my\x1b[mz') + + def test_replace(self): + s=AnsiStr('This string will be formatted italic and purple', ['purple', 'italic']) + s2 = s.replace('formatted', AnsiStr('formatted', 'bg_red')) + self.assertEqual(str(s2), '\x1b[38;5;90;3mThis string will be \x1b[0;41mformatted\x1b[0;38;5;90;3m italic and purple\x1b[m') + self.assertEqual(str(s), '\x1b[38;5;90;3mThis string will be formatted italic and purple\x1b[m') + + def test_split_whitespace(self): + s = AnsiStr('\t this \t\nstring contains\tmany\r\n\f\vspaces ', 'red', 'bold') + splits = s.split() + self.assertEqual( + [str(s) for s in splits], + ['\x1b[31;1mthis\x1b[m','\x1b[31;1mstring\x1b[m','\x1b[31;1mcontains\x1b[m','\x1b[31;1mmany\x1b[m','\x1b[31;1mspaces\x1b[m'] + ) + + def test_split_colon(self): + s = AnsiStr(':::this string: contains : colons:::', 'red', 'bold') + splits = s.split(':') + self.assertEqual( + [str(s) for s in splits], + ['', '', '', '\x1b[31;1mthis string\x1b[m', '\x1b[31;1m contains \x1b[m', '\x1b[31;1m colons\x1b[m', '', '', ''] + ) + + def test_rsplit(self): + s = AnsiStr(':::this string: contains : colons', 'red', 'bold') + splits = s.rsplit(':', 1) + self.assertEqual( + [str(s) for s in splits], + ['\x1b[31;1m:::this string: contains \x1b[m', '\x1b[31;1m colons\x1b[m'] + ) + + def test_splitlines(self): + s = AnsiStr('\nthis string\ncontains\nmany lines\n\n\n', 'red', 'bold') + splits = s.splitlines() + self.assertEqual( + [str(s) for s in splits], + ['', '\x1b[31;1mthis string\x1b[m', '\x1b[31;1mcontains\x1b[m', '\x1b[31;1mmany lines\x1b[m', '', ''] + ) + + def test_title(self): + s = AnsiStr('make this String a title for some book', 'red', 'bold') + s2 = s.title() + self.assertEqual(str(s2), '\x1b[31;1mMake This String A Title For Some Book\x1b[m') + self.assertEqual(str(s), '\x1b[31;1mmake this String a title for some book\x1b[m') + + def test_capitalize(self): + s = AnsiStr('make this String a title for some book', 'red', 'bold') + s2 = s.capitalize() + self.assertEqual(str(s2), '\x1b[31;1mMake this string a title for some book\x1b[m') + self.assertEqual(str(s), '\x1b[31;1mmake this String a title for some book\x1b[m') + + def test_cat_edge_case3(self): + # There was a bug when copy() was used and the string didn't start with any formatting + s = AnsiStr.join('This ', AnsiStr('string', AnsiFormat.ORANGE), ' contains ') + s = s + AnsiStr('multiple', AnsiFormat.BG_BLUE) + self.assertEqual(str(s), 'This \x1b[38;5;214mstring\x1b[m contains \x1b[44mmultiple\x1b[m') + + def test_format_matching(self): + s = AnsiStr('Here is a string that I will match formatting') + s = s.format_matching('InG', 'cyan', AnsiFormat.BG_PINK) + self.assertEqual( + str(s), + 'Here is a str\x1b[36;48;2;255;192;203ming\x1b[m that I will match formatt\x1b[36;48;2;255;192;203ming\x1b[m' + ) + + def test_format_matching_w_count1(self): + s = AnsiStr('Here is a string that I will match formatting') + s = s.format_matching('InG', 'cyan', AnsiFormat.BG_PINK, count=1) + self.assertEqual( + str(s), + 'Here is a str\x1b[36;48;2;255;192;203ming\x1b[m that I will match formatting' + ) + + def test_format_matching_ensure_escape(self): + s = AnsiStr('Here is a (string) that I will match formatting') + s = s.format_matching('(string)', 'cyan', AnsiFormat.BG_PINK) + self.assertEqual( + str(s), + 'Here is a \x1b[36;48;2;255;192;203m(string)\x1b[m that I will match formatting' + ) + + def test_format_matching_case_sensitive(self): + s = AnsiStr('Here is a strING that I will match formatting') + s = s.format_matching('ing', 'cyan', AnsiFormat.BG_PINK, match_case=True) + self.assertEqual( + str(s), + 'Here is a strING that I will match formatt\x1b[36;48;2;255;192;203ming\x1b[m' + ) + + def test_format_matching_regex_match_case(self): + s = AnsiStr('Here is a strING that I will match formatting') + s = s.format_matching('[A-Za-z]+ing', 'cyan', AnsiFormat.BG_PINK, regex=True, match_case=True) + self.assertEqual( + str(s), + 'Here is a strING that I will match \x1b[36;48;2;255;192;203mformatting\x1b[m' + ) + + def test_setting_eq_str(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertEqual(s.settings_at(0), '48;2;222;184;135') + + def test_rfind(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + result = s.rfind('ll') + self.assertEqual(result, 8) + + def test_find(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + result = s.find('ll') + self.assertEqual(result, 2) + + def test_index(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + result = s.index('ll') + self.assertEqual(result, 2) + + def test_rindex(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + result = s.rindex('ll') + self.assertEqual(result, 8) + + def test_upper(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + s2 = s.upper() + self.assertEqual( + str(s2), + '\x1b[38;2;0;0;0mHELLO HELLO\x1b[m' + ) + self.assertIsNot(s, s2) + + def test_lower(self): + s = AnsiStr('HELLO HELLO', AnsiFormat.rgb(0, 0, 0)) + s2 = s.lower() + self.assertEqual( + str(s2), + '\x1b[38;2;0;0;0mhello hello\x1b[m' + ) + self.assertIsNot(s, s2) + + def test_join_no_args(self): + s = AnsiStr.join() + self.assertEqual(str(s), '') + + def test_join_AnsiString_first_arg(self): + s = AnsiStr.join(AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0))) + self.assertEqual(str(s), '\x1b[38;2;0;0;0mhello hello\x1b[m') + + def test_in_w_str_true(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertIn('is', s) + + def test_in_w_str_false(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertNotIn('the', s) + + def test_in_w_AnsiString_true(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertIn(AnsiStr('is'), s) + + def test_in_w_AnsiString_false(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertNotIn(AnsiStr('the'), s) + + def test_eq_int(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertNotEqual(s, 1) + + def test_is_upper_true(self): + s = AnsiStr('HELLO HELLO', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isupper()) + + def test_is_upper_false(self): + s = AnsiStr('hELLO HELLO', AnsiFormat.rgb(0, 0, 0)) + self.assertFalse(s.isupper()) + + def test_is_lower_true(self): + s = AnsiStr('hello hello', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.islower()) + + def test_is_lower_false(self): + s = AnsiStr('Hello', AnsiFormat.rgb(0, 0, 0)) + self.assertFalse(s.islower()) + + def test_is_title_true(self): + s = AnsiStr('Hello Hello', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.istitle()) + + def test_is_title_false(self): + s = AnsiStr('Hello hello', AnsiFormat.rgb(0, 0, 0)) + self.assertFalse(s.istitle()) + + def test_is_space_true(self): + s = AnsiStr(' ', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isspace()) + + def test_is_printable_true(self): + s = AnsiStr(' dsfasdf', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isprintable()) + + def test_is_numeric_true(self): + s = AnsiStr('1', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isnumeric()) + + def test_is_digit_true(self): + s = AnsiStr('1', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isdigit()) + + def test_is_decimal_true(self): + s = AnsiStr('1', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isdecimal()) + + def test_is_identifier_true(self): + s = AnsiStr('AnsiStr', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isidentifier()) + + # Removed this test to be compatible with Python 3.6 + # def test_is_ascii_true(self): + # s = AnsiStr('1', AnsiFormat.rgb(0, 0, 0)) + # self.assertTrue(s.isascii()) + + def test_is_alpha_true(self): + s = AnsiStr('a', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isalpha()) + + def test_is_alnum_true(self): + s = AnsiStr('1', AnsiFormat.rgb(0, 0, 0)) + self.assertTrue(s.isalnum()) + + def test_expand_tabs(self): + s = AnsiStr('\ta\tb\n\tc', AnsiFormat.rgb(0, 0, 0)) + s = s.expandtabs(4) + self.assertEqual(str(s), '\x1b[38;2;0;0;0m a b\n c\x1b[m') + + def test_endswith(self): + s = AnsiStr('This is an ansi string', 'BG_BURLY_WOOD') + self.assertTrue(s.endswith('string')) + + def test_encode(self): + s = AnsiStr('Hello Hello', 'bold') + self.assertEqual(s.encode(), b'\x1b[1mHello Hello\x1b[m') + + def test_count(self): + s = AnsiStr('Hello Hello mmm', 'bold') + self.assertEqual(s.count('m'), 3) + + def test_base_str(self): + s = AnsiStr('Hello Hello', 'bold') + self.assertEqual(s.base_str, 'Hello Hello') + + def test_remove_settings(self): + s = AnsiStr('Hello Hello', 'bold', AnsiFormat.RED) + s = s.remove_formatting(AnsiFormat.BOLD, 2, 4) + self.assertEqual(str(s), '\x1b[1;31mHe\x1b[0;31mll\x1b[31;1mo Hello\x1b[m') + + def test_remove_settings_end(self): + s = AnsiStr('Hello Hello', 'bold', AnsiFormat.RED) + s = s.remove_formatting(AnsiFormat.BOLD, 2) + self.assertEqual(str(s), '\x1b[1;31mHe\x1b[0;31mllo Hello\x1b[m') + + def test_remove_settings_begin(self): + s = AnsiStr('Hello Hello', 'bold', AnsiFormat.RED) + s = s.remove_formatting(AnsiFormat.BOLD, end=2) + self.assertEqual(str(s), '\x1b[31mHe\x1b[31;1mllo Hello\x1b[m') + + def test_remove_settings_entire_range(self): + s = AnsiStr('Hello Hello', 'bold', AnsiFormat.RED) + s = s.remove_formatting(AnsiFormat.BOLD) + self.assertEqual(str(s), '\x1b[31mHello Hello\x1b[m') + + def test_remove_settings_entire_range_overlap(self): + s = AnsiStr('Hello Hello', AnsiFormat.RED) + s = s.apply_formatting(AnsiFormat.BOLD, 1, 3) + s = s.remove_formatting(AnsiFormat.BOLD) + self.assertEqual(str(s), '\x1b[31mHello Hello\x1b[m') + + def test_remove_settings_none(self): + s = AnsiStr('Hello Hello', AnsiFormat.RED) + s = s.remove_formatting(AnsiFormat.BOLD) + self.assertEqual(str(s), '\x1b[31mHello Hello\x1b[m') + + def test_remove_settings_multiple(self): + s = AnsiStr('Hello Hello', AnsiFormat.RED) + s = s.apply_formatting(AnsiFormat.RED, 1, -1) + s = s.remove_formatting(AnsiFormat.RED, 1, -1) + self.assertEqual(str(s), '\x1b[31mH\x1b[mello Hell\x1b[31mo\x1b[m') + + def test_remove_settings_outside_range(self): + s = AnsiStr('Hello Hello') + s = s.apply_formatting(AnsiFormat.RED, 0, 3) + s = s.remove_formatting(AnsiFormat.RED, start=-1) + self.assertEqual(str(s), '\x1b[31mHel\x1b[mlo Hello') + + def test_remove_settings_all(self): + s = AnsiStr('Hello Hello', AnsiFormat.RED) + s = s.apply_formatting(AnsiFormat.BOLD, 1, 3) + s = s.apply_formatting(AnsiFormat.BOLD, start=-1) + s = s.apply_formatting(AnsiFormat.RED, 0, 3) + s = s.remove_formatting(start=2) + self.assertEqual(str(s), '\x1b[31mH\x1b[31;1me\x1b[mllo Hello') + + def test_unformat_matching(self): + s = AnsiStr('Here is a string that I will unformat matching', AnsiFormat.CYAN, AnsiFormat.BOLD) + s = s.apply_formatting([AnsiFormat.BG_PINK], 38) + s = s.unformat_matching('ing', 'cyan', AnsiFormat.BG_PINK) + self.assertEqual( + str(s), + '\x1b[36;1mHere is a str\x1b[0;1ming\x1b[1;36m that I will unformat \x1b[1;36;48;2;255;192;203mmatch\x1b[0;1ming\x1b[m' + ) + + def test_unformat_matching_w_count1(self): + s = AnsiStr('Here is a string that I will unformat matching', AnsiFormat.CYAN, AnsiFormat.BOLD) + s = s.apply_formatting([AnsiFormat.BG_PINK], 38) + s = s.unformat_matching('ing', 'cyan', AnsiFormat.BG_PINK, count=1) + self.assertEqual( + str(s), + '\x1b[36;1mHere is a str\x1b[0;1ming\x1b[1;36m that I will unformat \x1b[1;36;48;2;255;192;203mmatching\x1b[m' + ) + + + # Exceptions tests + + def test_AnsiFormat_rgb_r_not_set(self): + with self.assertRaises(ValueError): + AnsiFormat.rgb(None) + + def test_AnsiFormat_rgb_g_without_b(self): + with self.assertRaises(ValueError): + AnsiFormat.rgb(100, 100) + + def test_AnsiFormat_rgb_b_without_g(self): + with self.assertRaises(ValueError): + AnsiFormat.rgb(100, b=100) + + def test_invalid_rgb_values1(self): + with self.assertRaises(ValueError): + AnsiStr('!', 'rgb(F)') + + def test_invalid_rgb_values3(self): + with self.assertRaises(ValueError): + AnsiStr('!', 'rgb(0,0,F)') + + def test_invalid_int(self): + with self.assertRaises(ValueError): + AnsiStr('!', -1) + + def test_invalid_name(self): + with self.assertRaises(ValueError): + AnsiStr('!', 'no setting') + + def test_getitem_invalid_step_size(self): + s = AnsiStr('!') + with self.assertRaises(ValueError): + s = s[0:1:2] + + def test_getitem_invalid_type(self): + s = AnsiStr('!') + with self.assertRaises(TypeError): + s = s[""] + + def test_string_format_sign_not_allowed(self): + s = AnsiStr('!') + with self.assertRaises(ValueError): + '{:^+10}'.format(s) + + def test_string_format_space_not_allowed(self): + s = AnsiStr('!') + with self.assertRaises(ValueError): + '{:^ 10}'.format(s) + + def test_string_format_invalid(self): + s = AnsiStr('!') + with self.assertRaises(ValueError): + '{:djhfjd}'.format(s) + + def test_cat_invalid_type(self): + s = AnsiStr('!') + with self.assertRaises(TypeError): + s += 1 + + def test_join_first_arg_invalid_type(self): + with self.assertRaises(TypeError): + AnsiStr.join(1) + + def test_simplify(self): + s = AnsiStr('abc', 'green') + s = s.apply_formatting('red') + s = s.simplify() + self.assertEqual(str(s), '\x1b[31mabc\x1b[m') + +if __name__ == '__main__': + unittest.main()