Skip to content

Commit

Permalink
Automatically perform optimization; fixed some bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
James Smith authored and James Smith committed Nov 17, 2024
1 parent 8fd25e0 commit ff100cf
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 36 deletions.
43 changes: 18 additions & 25 deletions src/ansi_string/ansi_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,11 @@ def __init__(
'''
# Key is the string index to make a color change at
self._fmts:Dict[int,'_AnsiSettingPoint'] = {}
self._optimize = False
self._s = ''

if isinstance(s, AnsiString):
for k, v in s._fmts.items():
self._fmts[k] = _AnsiSettingPoint(list(v.add), list(v.rem))
self._optimize = s._optimize
self._s = s._s
elif isinstance(s, str):
self.set_ansi_str(str(s))
Expand Down Expand Up @@ -329,18 +327,6 @@ def simplify(self):
'''
self.set_ansi_str(str(self))

def enable_output_optimization(self):
'''
This sets the output-optimization flag to True. Whenever the output string is generated, all formatting data
will be parsed, throwing out any data internally determined as invalid, and the most efficient output will be
generated based on all known parameters.
'''
self._optimize = True

def disable_output_optimization(self):
''' This sets the output-optimization flag to False. '''
self._optimize = False

def _shift_settings_idx(self, num:int, keep_origin:bool):
'''
Shifts format settings to the right by the given index
Expand Down Expand Up @@ -742,7 +728,9 @@ def _apply_string_format(self, string_format:str, settings:Union[AnsiFormat, Ans

def is_optimizable(self) -> bool:
'''
Returns True iff this object was not created with any verbatim setting strings that starts with "["
Returns True iff all of the following are true for this object:
- No verbatim setting strings (strings that starts with "[") were set in formatting
- All parsed settings are considered internally valid
'''
# Check if optimization is possible
for fmt in self._fmts.values():
Expand All @@ -751,7 +739,7 @@ def is_optimizable(self) -> bool:
return False
return True

def to_str(self, __format_spec:str=None, optimize:bool=None) -> str:
def to_str(self, __format_spec:str=None, optimize:bool=True) -> str:
'''
Returns an ANSI format string with both internal and given formatting spec set.
Parameters:
Expand All @@ -761,15 +749,12 @@ def to_str(self, __format_spec:str=None, optimize:bool=None) -> str:
ex: ">10:bold;red" to make output right justify with width of 10, bold and red formatting
No formatting should be applied as part of the justification, add a '-' after the fillchar.
ex: " ->10:bold;red" to not not apply formatting to justification characters
optimize - when set, overrides the internal output-optimization flag for this call
optimize - when true, attempt to optimize code strings
'''
if not __format_spec and not self._fmts:
# No formatting
return self._s

if optimize is None:
optimize = self._optimize

if __format_spec:
# Make a copy
obj = self.copy()
Expand Down Expand Up @@ -812,6 +797,7 @@ def to_str(self, __format_spec:str=None, optimize:bool=None) -> str:
# Settings were removed and there are settings to be applied -
# need to reset before applying current settings
settings_to_apply = [str(AnsiParam.RESET.value)] + settings_to_apply
apply_to_out_str = True
codes_str = ansi_sep.join(settings_to_apply)
if optimize:
old_settings_dict = current_settings_dict
Expand All @@ -822,13 +808,21 @@ def to_str(self, __format_spec:str=None, optimize:bool=None) -> str:
if key not in new_settings_dict:
# Add the param that will clear this setting
settings_to_apply.append(str(EFFECT_CLEAR_DICT[key].value))
settings_to_apply += [str(value) for key, value in new_settings_dict.items() if key not in old_settings_dict]
settings_to_apply += [
str(value)
for key, value in new_settings_dict.items()
if key not in old_settings_dict or old_settings_dict[key] != value
]
optimized_codes_str = ansi_sep.join(settings_to_apply)
# Empty optimized codes string here just means the previous settings should be maintained, not deleted
if not optimized_codes_str:
apply_to_out_str = False
# This check is necessary because sometimes the optimization will actually create a longer string
if len(optimized_codes_str) < len(codes_str):
elif len(optimized_codes_str) < len(codes_str):
codes_str = optimized_codes_str
# Apply these settings
out_str += ansi_graphic_rendition_format.format(codes_str)
if apply_to_out_str:
out_str += ansi_graphic_rendition_format.format(codes_str)
# Save this flag in case this is the last loop
clear_needed = bool(current_settings)

Expand Down Expand Up @@ -1832,7 +1826,7 @@ def __new__(
*settings:Union[AnsiFormat, AnsiSetting, str, int, list, tuple]
):
if isinstance(s, AnsiString):
ansi_string = s
ansi_string = s.copy()
elif isinstance(s, AnsiStr):
if settings:
ansi_string = AnsiString(s, *settings)
Expand All @@ -1845,7 +1839,6 @@ def __new__(
else:
raise TypeError('Invalid type for s')
ansi_string.simplify()
ansi_string.enable_output_optimization()
instance = super().__new__(cls, str(ansi_string))
instance._s = ansi_string.base_str
return instance
Expand Down
22 changes: 11 additions & 11 deletions tests/test_ansi_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def test_ranges(self):
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 '
'This \x1b[1mstring\x1b[m contains \x1b[44;38;5;214;3mmultiple\x1b[49m color\x1b[m settings '
'across different ranges'
)

Expand Down Expand Up @@ -421,12 +421,12 @@ def test_iterate(self):
def test_apply_string_equal_length(self):
s = AnsiString('a', 'red') + AnsiString('b', 'green') + AnsiString('c', 'blue')
s.assign_str('xyz')
self.assertEqual(str(s), '\x1b[31mx\x1b[0;32my\x1b[0;34mz\x1b[m')
self.assertEqual(str(s), '\x1b[31mx\x1b[32my\x1b[34mz\x1b[m')

def test_apply_larger_string(self):
s = AnsiString('a', 'red') + AnsiString('b', 'green') + AnsiString('c', 'blue')
s.assign_str('xxxxxx')
self.assertEqual(str(s), '\x1b[31mx\x1b[0;32mx\x1b[0;34mxxxx\x1b[m')
self.assertEqual(str(s), '\x1b[31mx\x1b[32mx\x1b[34mxxxx\x1b[m')

def test_apply_shorter_string(self):
s = AnsiString('a', 'red') + AnsiString('b', 'green') + AnsiString('c', 'blue')
Expand All @@ -440,7 +440,7 @@ def test_remove_prefix_inplace(self):
s.apply_formatting(AnsiFormat.BEIGE, 3, 4)
s.apply_formatting(AnsiFormat.BG_DARK_GRAY, 4, 5)
s2 = s.removeprefix('blah', inplace=True)
self.assertEqual(str(s), '\x1b[14;48;2;169;169;169m \x1b[0;14mblah\x1b[m')
self.assertEqual(str(s), '\x1b[14;48;2;169;169;169m \x1b[49mblah\x1b[m')
self.assertIs(s, s2)

def test_remove_prefix_not_found(self):
Expand Down Expand Up @@ -475,7 +475,7 @@ def test_cat_edge_case2(self):
b.apply_formatting('red', end=-2)
b.apply_formatting('bold', end=-1)
c = a + b
self.assertEqual(str(c), '\x1b[31;1mabcx\x1b[0;1my\x1b[mz')
self.assertEqual(str(c), '\x1b[31;1mabcx\x1b[39my\x1b[mz')

def test_replace_inplace(self):
s=AnsiString('This string will be formatted italic and purple', ['purple', 'italic'])
Expand Down Expand Up @@ -771,17 +771,17 @@ def test_base_str(self):
def test_remove_settings(self):
s = AnsiString('Hello Hello', 'bold', AnsiFormat.RED)
s.remove_formatting(AnsiFormat.BOLD, 2, 4)
self.assertEqual(str(s), '\x1b[1;31mHe\x1b[0;31mll\x1b[31;1mo Hello\x1b[m')
self.assertEqual(str(s), '\x1b[1;31mHe\x1b[22mll\x1b[1mo Hello\x1b[m')

def test_remove_settings_end(self):
s = AnsiString('Hello Hello', 'bold', AnsiFormat.RED)
s.remove_formatting(AnsiFormat.BOLD, 2)
self.assertEqual(str(s), '\x1b[1;31mHe\x1b[0;31mllo Hello\x1b[m')
self.assertEqual(str(s), '\x1b[1;31mHe\x1b[22mllo Hello\x1b[m')

def test_remove_settings_begin(self):
s = AnsiString('Hello Hello', 'bold', AnsiFormat.RED)
s.remove_formatting(AnsiFormat.BOLD, end=2)
self.assertEqual(str(s), '\x1b[31mHe\x1b[31;1mllo Hello\x1b[m')
self.assertEqual(str(s), '\x1b[31mHe\x1b[1mllo Hello\x1b[m')

def test_remove_settings_entire_range(self):
s = AnsiString('Hello Hello', 'bold', AnsiFormat.RED)
Expand Down Expand Up @@ -817,15 +817,15 @@ def test_remove_settings_all(self):
s.apply_formatting(AnsiFormat.BOLD, start=-1)
s.apply_formatting(AnsiFormat.RED, 0, 3)
s.remove_formatting(start=2)
self.assertEqual(str(s), '\x1b[31;31mH\x1b[31;31;1me\x1b[mllo Hello')
self.assertEqual(str(s), '\x1b[31mH\x1b[1me\x1b[mllo Hello')

def test_unformat_matching(self):
s = AnsiString('Here is a string that I will unformat matching', AnsiFormat.CYAN, AnsiFormat.BOLD)
s.apply_formatting([AnsiFormat.BG_PINK], 38)
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'
'\x1b[36;1mHere is a str\x1b[39ming\x1b[36m that I will unformat \x1b[48;2;255;192;203mmatch\x1b[0;1ming\x1b[m'
)

def test_unformat_matching_w_count1(self):
Expand All @@ -834,7 +834,7 @@ def test_unformat_matching_w_count1(self):
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'
'\x1b[36;1mHere is a str\x1b[39ming\x1b[36m that I will unformat \x1b[48;2;255;192;203mmatching\x1b[m'
)


Expand Down

0 comments on commit ff100cf

Please sign in to comment.