def test_color(self): """ Colr.color should accept valid color names/values. """ # None of these should raise a InvalidColr. s = 'test' try: Colr(s, 'red') Colr(s, 16) Colr(s, (255, 0, 0)) except InvalidColr as ex: self.fail( 'InvalidColr raised for valid color name/value: {}'.format(ex)) # Should get the correct code type for the correct value. self.assertTrue(is_code(str(Colr(' ', 'red')).split()[0])) self.assertTrue(is_ext_code(str(Colr(' ', 56)).split()[0])) self.assertTrue(is_rgb_code(str(Colr(' ', (0, 0, 255))).split()[0])) # Should raise InvalidColr on invalid color name/value. with self.assertRaises(InvalidColr): Colr(s, 'NOTACOLOR') with self.assertRaises(InvalidColr): Colr(s, 257) with self.assertRaises(InvalidColr): Colr(s, (-1, 0, 0)) with self.assertRaises(InvalidColr): Colr(s, (257, 0, 0))
def test_format_raises(self): """ Colr.__format__ should raise InvalidFormatColr on bad colors. """ bad_args = ( # Invalid fore name. { 'fore': 'not_a_color' }, # Invalid back name. { 'back': 'not_a_color' }, # Invalid style name. { 'fore': 'red', 'back': 'black', 'style': 'not_a_style' }, ) test_key = 'x' for args in bad_args: spec = self.format_spec_from_args(test_key, **args) raisechk = self.assertCallRaises( InvalidFormatColr, func=spec.format, kwargs={test_key: Colr('Test')}, msg='Failed to raise for spec: {!r}'.format(spec), ) with raisechk: spec.format(**{test_key: Colr('Test')}) bad_args = ( # Invalid RGB (should be 0;0;0) { 'fore': 'red', 'back': '0,0,0' }, # Invalid RGB (should be 0;0;0) { 'fore': '0,0,0', 'back': 'black' }, # Invalid style. { 'fore': 'red', 'back': 'black', 'style': '0,0,0' }, ) test_key = 'x' for args in bad_args: spec = self.format_spec_from_args(test_key, **args) raisechk = self.assertCallRaises( InvalidFormatArg, func=spec.format, kwargs={test_key: Colr('Test')}, msg='Failed to raise for spec: {!r}'.format(spec), ) with raisechk: spec.format(**{test_key: Colr('Test')})
def test_gradient(self): """ Colr.gradient should recognize names and rainbow offsets. """ valid_names = list(Colr.gradient_names) valid_names.extend(range(1, 254)) valid_names.extend((False, True, None, '', 0, 1.2)) for valid_name in valid_names: try: Colr('test').gradient(name=valid_name) except ValueError: self.fail( self.call_msg( 'Colr.gradient failed on a known name.', valid_name, func=Colr().gradient, )) invalid_names = ('block', 'bash', 'berry') for invalid_name in invalid_names: try: Colr('test').gradient(name=invalid_name) except ValueError: pass else: self.fail( self.call_msg( 'Failed to raise on invalid name.', invalid_name, func=Colr().gradient, ))
def test_getitem(self): """ Colr.__getitem__ should grab escape codes before and after. """ # Simple string indexing, with color codes. exampleargs = self.example_args() # Reset styles should be kept at the start of a Colr. for stylename in codes['style']: exampleargs['style_{}'.format(stylename)] = { 'fore': self.random_color(), 'back': self.random_color(), 'style': stylename, } for argtype, kwargs in exampleargs.items(): index = random.randint(0, len(argtype) - 1) clr = Colr(argtype, **kwargs) clr_s = clr[index] self.assertCallEqual( clr_s, Colr(argtype[index], **kwargs), func=Colr.__getitem__, args=( clr, index, ), kwargs=kwargs, msg='Failed to keep color codes for __getitem__.', )
def display_test_gradient_rgb(maxwidth=80): """ Test the gradient_rgb method. """ lines = [ 'This is a block of text made into a gradient, rgb style.' for _ in range(10) ] print( Colr('\n'.join(lines)).gradient_rgb( start=(240, 0, 255), stop=(188, 255, 0), step=1, linemode=True, movefactor=-25, )) linetext = ' This is a block made into a long gradient, rgb style. ' lines = [linetext.center(maxwidth, 'X') for _ in range(10)] print( Colr('\n'.join(lines)).gradient_rgb( start=(0, 121, 255), stop=(191, 0, 182), step=1, linemode=False, )) print(Colr().gradient_rgb( ' ' * maxwidth, start=(0, 0, 0), stop=(255, 255, 255), fore='reset', ))
def display_test_rainbow(maxwidth=80): """ Test rainbow output, with or without linemode (short/long output) """ print(Colr('This is a rainbow. It is very pretty.').rainbow()) lines = ['This is a block of text made into a rainbow' for _ in range(5)] print(Colr('\n'.join(lines)).rainbow(movefactor=5)) lines = [ 'This is a block of text made into a rainbow (rgb mode)' for _ in range(5) ] print(Colr('\n'.join(lines)).rainbow(movefactor=5, rgb_mode=True)) lines = ['This is a block made into a long rainbow' for _ in range(5)] print(Colr('\n'.join(lines)).rainbow(linemode=False, rgb_mode=False)) lines = [ 'This is a block made into a long rainbow (rgb mode)' for _ in range(5) ] print(Colr('\n'.join(lines)).rainbow(linemode=False, rgb_mode=True)) # Rainbow should honor fore,back,styles. print(Colr(' ' * maxwidth).rainbow(fore='reset', spread=.5)) print(Colr('-' * maxwidth).rainbow(back='black', offset=30)) print(Colr(' ' * maxwidth).rainbow(fore='reset', spread=.5, rgb_mode=True)) print(Colr('-' * maxwidth).rainbow(back='black', offset=30, rgb_mode=True)) print(Colr('Rainbow bright.').rainbow(style='bright').center(maxwidth))
def _equality_msg(op, a, b, msg=None): """ The ne_msg and eq_msg wrap this function to reduce code duplication. It builds a message suitable for an assert*Equal msg parameter. """ fmta = str(Colr(repr(a), 'yellow')) try: if repr(a) != str(a): fmta = '{} ({}) {}'.format(fmta, a, type(a).__name__) except TypeError as ex: # str() returned non-string type. Catch it now, instead of pushing # to PyPi. raise TypeError('{} (A value) (type: {}) (value: {!r})'.format( ex, type(a).__name__, a, )) fmtb = str(Colr(repr(b), 'green')) try: if repr(b) != str(b): fmtb = '{} ({}) {}'.format(fmtb, b, type(b).__name__) except TypeError as ex: # str() returned non-string type. Catch it now, instead of pushing # to PyPi. raise TypeError('{} (B value) (type: {}) (value: {!r})'.format( ex, type(b).__name__, b, )) return '\n'.join(( '\n {} {}'.format(fmta, Colr(op, 'red', style='bright')), ' {}'.format(fmtb), '\n{}'.format(Colr(msg, 'red')) if msg else '', ))
def test_add(self): """ Colrs should be added to each other, Controls, or strs. """ types = { 'Colr': Colr('Test', 'red'), 'Control': Control().move_down(1), 'str': 'testing', } for othername, other in types.items(): clr = Colr('Testing', 'blue') try: newclr = clr + other except TypeError as ex: self.fail('Colr + {} should not raise a TypeError.'.format( othername)) else: self.assertIsInstance( newclr, Colr, msg=('Adding {} to a Colr did not return a Colr.' ).format(othername)) clr_str_result = ''.join((str(clr), str(other))) s = str(newclr) self.assertEqual(clr_str_result, s, msg='str(Colr()) did not match.')
def test_hex_rgb_mode(self): """ Colr.hex should use true color when rgb_mode is True. """ s = 'test' # Hex values with rgb_mode=True should do a straight conversion. hexrgb = { 'bada55': (186, 218, 85), 'cafeba': (202, 254, 186), '858585': (133, 133, 133), '010203': (1, 2, 3), } for hexval, rgbval in hexrgb.items(): hexcolr = Colr().hex(hexval, s, rgb_mode=True) rgbcolr = Colr().rgb(*rgbval, s) self.assertCallEqual( hexcolr, rgbcolr, func=Colr.hex, args=[hexval, s], kwargs={'rgb_mode': True}, msg='Chained hex in rgb_mode did not match rgb.', ) hexcolr = Colr(s).b_hex(hexval, rgb_mode=True) rgbcolr = Colr(s).b_rgb(*rgbval) self.assertCallEqual( hexcolr, rgbcolr, func=Colr.b_hex, args=[hexval], kwargs={'rgb_mode': True}, msg='Chained b_hex in rgb_mode did not match b_rgb.', )
def test_hash(self): """ hash(Colr()) should return a unique hash for self.data. """ a, b = hash(Colr('test', 'red')), hash(Colr('test', 'red')) self.assertEqual(a, b, msg=ne_msg(a, b, msg='Mismatched hash values.')) a, b = hash(Colr('test', 'red')), hash(Colr('test', 'blue')) self.assertNotEqual(a, b, msg=eq_msg(a, b, msg='Hash values should not match.'))
def display_test_gradient_mix(maxwidth=80): """ Test display of the gradient options. """ # Gradient should operate on self.data when no text is provided. print(Colr('This is a gradient self.data.').gradient()) # Gradient should append to self.data when no text is provided. print( Colr('This is a green self.data', fore='green')(' ').gradient('And this is an appended gradient.', name='blue')) # Gradient should be okay with ljust/center/rjust. print(Colr().gradient('This is a left gradient').ljust(maxwidth)) print(Colr().gradient('Center gradient.').center(maxwidth)) print(Colr().gradient('Right-aligned gradient.').rjust(maxwidth)) # Gradient and ljust/center/rjust would be chainable. chunkwidth = maxwidth / 3 print(Colr().ljust( chunkwidth, text='Chained left.').gradient(name='red').center( chunkwidth, text='Chained center.').gradient(name='white').rjust( chunkwidth, text='Chained right.').gradient(name='blue')) # Black/white gradient should work in linemode or non-linemode. lines = ['This is a block made into a sad rainbow' for _ in range(5)] print(Colr('\n'.join(lines)).gradient(name='black')) lines = ['This is a block made into a long sad rainbow' for _ in range(5)] print(Colr('\n'.join(lines)).gradient(name='white', linemode=False)) lines = ['This is a block made into a better rainbow' for _ in range(5)] print(Colr('\n'.join(lines)).gradient(name='red'))
def test_name_data_attr(self): """ Colr should recognize fg_<name_data> and bg_<name_data> attrs. """ # This will raise an AttributeError if name_data isn't working. self.assertIsInstance( Colr().f_aliceblue('test'), Colr, msg='Failed to create Colr from chained name_data method.') self.assertIsInstance( Colr().b_antiquewhite('test'), Colr, msg='Failed to create Colr from chained name_data method.')
def _equality_msg(op, a, b, msg=None): """ The ne_msg and eq_msg wrap this function to reduce code duplication. It builds a message suitable for an assert*Equal msg parameter. """ # TODO: Just subclass/patch unittest.assertEqual and friends. return '\n'.join(( '\n {} {}'.format(Colr(repr(a), 'yellow'), Colr(op, 'red', style='bright')), ' {}'.format(Colr(repr(b), 'green')), '\n{}'.format(Colr(msg, 'red')) if msg else '', ))
def test_color_colr_typeerror(self): """ Colr.color should raise TypeError when __colr__ returns non Colrs. """ try: Colr(CustomUserClass()) except TypeError as ex: msg = 'Shouldn\'t raise TypeError for valid __colr__ method.' self.fail('\n'.join((msg, str(ex)))) with self.assertRaises(TypeError): Colr(CustomUserClassBad())
def test_color_invalid(self): """ Colr.color should raise InvalidColr on invalid color name/value. """ s = 'test' with self.assertRaises(InvalidColr): Colr(s, 'NOTACOLOR') with self.assertRaises(InvalidColr): Colr(s, 257) with self.assertRaises(InvalidColr): Colr(s, (-1, 0, 0)) with self.assertRaises(InvalidColr): Colr(s, (257, 0, 0))
def test_hex(self): """ Colr.color should recognize hex colors. """ s = 'test' # Short/long hex values with/without hash should work. hexcodes = { 'fff': 'short hex color without a hash', 'ffffff': 'hex color without a hash', '#fff': 'short hex color', '#ffffff': 'hex color', } for hexcolr, desc in hexcodes.items(): try: hexcolr = Colr(s, hexcolr) except InvalidColr as ex: self.fail('Failed to recognize {}.'.format(desc)) termcolr = Colr(s, 231) self.assertEqual( hexcolr, termcolr, msg='Basic hex color did not match closest term color.', ) # Hex values with rgb_mode=False should produce a close match. closematches = { 'd7d7ff': 189, '008787': 30, 'afd75f': 149, '000': 16, '000000': 16, 'fff': 231, 'ffffff': 231, } for hexval in sorted(closematches): closeterm = closematches[hexval] closetermcolr = Colr(s, closeterm) for hexvalue in (hexval, '#{}'.format(hexval)): hexcolr = Colr(s, hexvalue) self.assertCallEqual( hexcolr, closetermcolr, func=Colr.color, args=[s, hexvalue], msg='Hex value is not the same output as close term.', ) chainedhexcolr = Colr().hex(hexvalue, s, rgb_mode=False) self.assertCallEqual( chainedhexcolr, closetermcolr, func=Colr.hex, args=[hexval, s], kwargs={'rgb_mode': False}, msg='Chained hex value is not the same as close term.')
def test_hex_colors(self): """ colr tool should recognize hex colors. """ argd = {'TEXT': 'Hello World', 'FORE': 'd7d7d7'} for _ in range(10): argd['FORE'] = r.choice(self.valid_hex_vals) argd['BACK'] = r.choice(self.valid_hex_vals) self.assertMain(argd, msg='main() failed on hex colors.') # Without -T, close matches should be used. argd = {'TEXT': 'Hello World', '--truecolor': False} hexvals = { '010203': '000000', '040506': '000000', } for hexval, closematch in hexvals.items(): argd['FORE'] = hexval self.assertEqual( get_colr(argd['TEXT'], self.make_argd(argd)), Colr(argd['TEXT'], closematch), msg='Hex value close match failed without --truecolor.', ) # With -T, rgb mode should be used. argd = {'TEXT': 'Hello World', '--truecolor': True} hexvals = ( '010203', '040506', ) for hexval in hexvals: argd['FORE'] = hexval self.assertEqual( get_colr(argd['TEXT'], self.make_argd(argd)), Colr(argd['TEXT'], hex2rgb(argd['FORE'])), msg='Hex value failed with --truecolor.', ) # Invalid color values should raise a InvalidColr. argd = {'TEXT': 'Hello World', '--truecolor': False} badargsets = ( { 'FORE': 'ffooll', 'BACK': r.choice(self.valid_hex_vals) }, { 'BACK': 'oopsie', 'FORE': r.choice(self.valid_hex_vals) }, ) for argset in badargsets: argd.update(argset) with self.assertRaises(InvalidColr): self.run_main_test(argd, should_fail=True)
def call_msg(s: str, *args: Any, **kwargs: Mapping[Any, Any]): """ Return a message suitable for the `msg` arg in asserts, including the calling function name. """ if s.count(':') == 1: stdmsg, _, msg = s.partition(':') # type: ignore else: stdmsg, msg = s, None # type: ignore return '{funcsig}: {stdmsg}{msgdiv}{msg}'.format( funcsig=format_call_str(*args, **kwargs), stdmsg=Colr(stdmsg, 'red', style='bright'), msgdiv=': ' if msg else '', msg=Colr(msg, 'red'), )
def test_indent(self): """ Colr.indent should indent a char, str, or Colr. """ colrnames = ('red', 'blue', 'black', 'white') for i, name in enumerate(colrnames): n = (i + 1) * 2 clr = Colr('test', name) clr2 = clr.copy() self.assertCallEqual( clr.indent(n, char=' '), Colr('{}{}'.format(' ' * n, clr2)), func=Colr.indent, args=(n, ' '), msg='Failed to indent properly.', )
def display_test_justify(maxwidth=80): """ Test the justification methods, alone and mixed with other methods. """ # Justified text should be chainable. chunkwidth = maxwidth / 80 print(Colr().ljust(chunkwidth, text='Left', fore=255, back='green', style='b').center(chunkwidth, text='Middle', fore=255, back='blue', style='b').rjust(chunkwidth, text='Right', fore=255, back='red', style='b')) # Chained formatting must provide the 'text' argument, # otherwise the string is built up and the entire string width grows. # This built up string would then be padded, instead of each individual # string. print(Colr() # 256 color methods can be called with bg_<num>, b_<num>, b256_<num>. .b_82().b().f_255().ljust(chunkwidth, text='Left').b256_56().b().f_255().center( chunkwidth, text='Middle') # Named background color start with 'bg' or 'b_' .bgred().b().f_255().rjust(chunkwidth, text='Right')) # Width should be calculated without color codes. print(Colr('True Middle').center(maxwidth, fore='magenta')) # Squeezed justification should account for existing text width. # But if text was previously justified, don't ruin it. print( Colr('Lefty', fore=232, back=255).center(maxwidth, text='Center', fore=232, back='blue', style='bright', squeeze=True)) print( Colr('LeftyCenter'.center(maxwidth // 2), fore=232, back=255).center(maxwidth / 2, text='Center', fore=232, back='blue', style='bright', squeeze=True))
def test_append(self): """ Colr.append should append a char, str, or Colr. """ colrnames = ('red', 'blue', 'black', 'white') for i, name in enumerate(colrnames): n = (i + 1) * 2 clr = Colr('test', name) clr2 = clr.copy() self.assertCallEqual( clr.append(' ', length=n), Colr('{}{}'.format(clr2, ' ' * n)), func=Colr.append, args=(' ', n), msg='Failed to append properly.', )
def test_color_colr(self): """ Colr.color should honor __colr__ methods. """ customtext = 'test' custom = CustomUserClass(customtext) try: clr = Colr(custom) except InvalidColr as ex: self.fail( 'InvalidColr raised for valid custom class: {}'.format(ex)) self.assertEqual( clr, Colr(customtext, **CustomUserClass.default_args), msg='Colr.color failed for custom class with __colr__ method.', )
def display_test_gradient_override(maxwidth=80): """ Test gradient with explicit fore, back, and styles. """ try: # Both fore and back are not allowed in a gradient. print(Colr().gradient(' ' * maxwidth, fore='reset', back='reset')) except ValueError: pass # Gradient back color. print(Colr().gradient(' ' * maxwidth, name='black', fore='reset')) # Explicit gradient fore color. print(Colr().gradient('-' * maxwidth, name='white', spread=2, back='blue')) # Implicit gradient fore color. print(Colr().gradient('_' * maxwidth, name='white'), end='\n\n')
def display_test_square_brackets(maxwidth=80): """ Test how colr acts when square brackets are mixed with color codes. """ colornames = list(codes['fore']) teststrings = ( (']', ), (']' * 20, ), ('[bracket test]', ), ('[bracket test]', 'this'), ('[bracket test]', 'this', '[thing]'), ) for strings in teststrings: print( Colr(' ').join( Colr(s, random.choice(colornames)) for s in strings))
def fancy_log(label, msg, tag): """ Squeezed justification with complex joins should account for existing text width. """ return (Colr(label, fore='green').center( # Centering on maxwidth would ruin the next rjust because # the spaces created by .center will not be overwritten. maxwidth - (len(tag) + 2), text=msg, fore='yellow', squeeze=True).rjust(maxwidth, text=Colr(tag, fore='red').join('[', ']', fore='blue'), squeeze=True))
def test_strip(self): """ Colr.strip should strip characters and return another Colr. """ teststrings = ( (None, ' test ', 'test'), (' ', ' test ', 'test'), ('X', 'XXXXtestXXXX', 'test'), ('Xx', 'XxxXXxXxXXxtestXXxxXxxXxX', 'test'), (closing_code, ''.join( (closing_code, 'test', closing_code)), 'test'), ) for chars, s, expected in teststrings: clr = Colr(s) stripped = clr.strip(chars) self.assertCallEqual( str(stripped), expected, func=Colr.strip, args=(clr, ), kwargs={'chars': chars}, msg='Failed to strip characters.', ) self.assertCallIsInstance( stripped, Colr, func=Colr.strip, args=(clr, ), kwargs={'chars': chars}, msg='Did not return a Colr instance.', ) testcolrs = [] for chars, s, expected in teststrings: if chars == closing_code: # Cannot strip closing code from a colorized closing_code. continue for argtype, kwargs in self.example_args().items(): testcolrs.append( (chars, Colr(s, **kwargs), Colr(expected, **kwargs))) for chars, clr, expected in testcolrs: stripped = clr.strip(chars) self.assertCallEqual( stripped, expected, func=Colr.strip, args=(clr, ), kwargs={'chars': chars}, msg='Failed to strip characters from colorized Colr.', )
def _equality_msg(op, a, b, msg=None): """ The ne_msg and eq_msg wrap this function to reduce code duplication. It builds a message suitable for an assert*Equal msg parameter. """ fmta = str(Colr(repr(a), 'yellow')) if repr(a) != str(a): fmta = '{} ({})'.format(fmta, a) fmtb = str(Colr(repr(b), 'green')) if repr(b) != str(b): fmtb = '{} ({})'.format(fmtb, b) return '\n'.join(( '\n {} {}'.format(fmta, Colr(op, 'red', style='bright')), ' {}'.format(fmtb), '\n{}'.format(Colr(msg, 'red')) if msg else '', ))
def test_stripped(self): """ Colr.stripped() should return strip_codes(Colr()). """ data = 'This is a test.' c = Colr(data, fore='red', style='bright') datalen = len(data) stripped = c.stripped() strippedlen = len(stripped) self.assertEqual( datalen, strippedlen, call_msg( 'Stripped Colr has different length.', datalen, strippedlen, ), ) self.assertEqual( data, stripped, call_msg( 'Stripped Colr has different content.', data, stripped, ), )
def test_color_colr_override(self): """ Colr.color should override __colr__ methods when asked. """ # Overriding the Colr call args disables __colr__ method. customtext = 'test' custom = CustomUserClass(customtext) customargs = {'fore': 'red', 'back': 'white', 'style': 'underline'} try: clr = Colr(custom, **customargs) except InvalidColr as ex: self.fail( 'InvalidColr raised for valid custom class: {}'.format(ex)) self.assertEqual( clr, Colr(customtext, **customargs), msg='Colr.color failed to override custom class __colr__ method.', )
def log_all(self): """Log all errors in the list with a header.""" print(Colr("ERRORS: ", fore="red", style="bold")) for err in self.errors: print(err)