Beispiel #1
0
    def test__constructor__bad_data_field(self):
        with self.assertRaises(ValueError) as context:
            print(NiceTable())
        self.assertTrue(
            str(context.exception) ==
            'NiceTable(): provide at least one of the following parameters: data, col_names',
            'correctly raises when both col_names and data are missing')

        with self.assertRaises(TypeError) as context:
            print(NiceTable(data='cat'))
        self.assertTrue(
            str(context.exception) ==
            "NiceTable(): data parameter expecting a list, got <class 'str'>",
            'correctly raises if data is not a list')

        with self.assertRaises(TypeError) as context:
            print(NiceTable(data=['cat']))
        self.assertTrue(
            str(context.exception) ==
            "NiceTable(): when generating column names, data parameter should be "
            "a list of lists/tuples or a list of dicts, but got a list item of type <class 'str'>",
            'correctly raises if data list has an element that is not a list/tuple/dict'
        )

        with self.assertRaises(TypeError) as context:
            print(out=NiceTable(data=[[1, 2, 3], {'x': 1, 'y': 2}]))
        self.assertTrue(
            str(context.exception) ==
            'NiceTable(): data parameter expecting either a list of lists/tuples'
            ' or a list of dicts, got a list that mixes dicts with lists/tuples',
            'correctly raises if data list mixes dicts with lists/tuples')
Beispiel #2
0
 def test__constructor__data_only__list_of_dicts(self):
     out1 = NiceTable(json.loads(NiceTable.SAMPLE_JSON))
     out2 = NiceTable(col_names=['id', 'name', 'type', 'height', 'weight'])
     for row in json.loads(NiceTable.SAMPLE_JSON):
         out2.append(row)
     self.assertEqual(
         str(out1), str(out2),
         'passing a list of dicts correctly auto-generates column names')
Beispiel #3
0
 def test__constructor__data_only__list_of_lists(self):
     out1 = NiceTable(NiceTable.FORMATTING_SETTINGS)
     out2 = NiceTable(col_names=['c001', 'c002', 'c003', 'c004'])
     for row in NiceTable.FORMATTING_SETTINGS:
         out2.append(row)
     self.assertEqual(
         str(out1), str(out2),
         'passing a list of lists correctly auto-generates column names')
Beispiel #4
0
 def test__append_bad_type(self):
     with self.assertRaises(TypeError) as context:
         out = NiceTable(NiceTable.builtin_layouts(),
                         col_names=['Layout', 'Description'])
         out.append(123)
     self.assertTrue(
         str(context.exception) == "NiceTable.append(): "
         "expecting a list / dict / tuple / None, got <class 'int'>",
         "append() accepts None or list/dict/tuple")
Beispiel #5
0
 def test__constructor__data_only__list_of_tuples(self):
     tuples = [("apple", "banana", "cherry"), ("dog", "cat")]
     out1 = NiceTable(tuples)
     out2 = NiceTable(col_names=['c001', 'c002', 'c003'])
     for row in tuples:
         out2.append(row)
     self.assertEqual(
         str(out1), str(out2),
         'passing a list of tuples correctly auto-generates column names')
Beispiel #6
0
 def test__constructor__col_names_and_data__list_of_dict(self):
     out1 = NiceTable(json.loads(NiceTable.SAMPLE_JSON),
                      col_names=['id', 'name', 'type', 'height', 'weight'])
     out2 = NiceTable(col_names=['id', 'name', 'type', 'height', 'weight'])
     for layout in json.loads(NiceTable.SAMPLE_JSON):
         out2.append(layout)
     self.assertEqual(
         str(out1), str(out2),
         'initializing NiceTable with a list of dicts is the same as appending each dict in a loop'
     )
Beispiel #7
0
 def setUp(
         self):  # TODO: maybe replace with a factory class like factory_boy
     # all layout options tests starts with the same table data:
     self.tbl = NiceTable(
         col_names=['Name', 'Type', 'Height(cm)', 'Weight(kg)'])
     for pokemon in json.loads(NiceTable.SAMPLE_JSON):
         self.tbl.append([
             pokemon['name'], pokemon['type'], pokemon['height'],
             pokemon['weight']
         ])
Beispiel #8
0
    def test__append_dict(self):
        out1 = NiceTable(
            col_names=['name', 'What is this', 'height', 'weight'])
        out2 = NiceTable(
            col_names=['Name', 'Type', 'Height(cm)', 'Weight(kg)'])
        for pokemon in json.loads(NiceTable.SAMPLE_JSON):
            out1.append(pokemon)
            out2.append(pokemon)

        expected_out = \
            '+-------------+----------------+----------+-----------+\n' + \
            '|  name       |  What is this  |  height  |  weight   |\n' + \
            '+-------------+----------------+----------+-----------+\n' + \
            '|  Bulbasaur  |          None  |      70  |    6.901  |\n' + \
            '|  Pikachu    |          None  |      40  |    6.100  |\n' + \
            '|  Mewtwo     |          None  |     200  |  122.000  |\n' + \
            '+-------------+----------------+----------+-----------+\n'
        self.assertEqual(
            expected_out, str(out1),
            'dict fields that matches column names should be appended, else None '
        )

        expected_out = \
            '+--------+--------+--------------+--------------+\n' \
            '|  Name  |  Type  |  Height(cm)  |  Weight(kg)  |\n' \
            '+--------+--------+--------------+--------------+\n' \
            '|  None  |  None  |        None  |        None  |\n' \
            '|  None  |  None  |        None  |        None  |\n' \
            '|  None  |  None  |        None  |        None  |\n' \
            '+--------+--------+--------------+--------------+\n'

        self.assertEqual(
            expected_out, str(out2),
            'append with dict works even if no field is matching')
Beispiel #9
0
    def test__rename_columns(self):
        out = NiceTable(data=json.loads(NiceTable.SAMPLE_JSON))
        with self.assertRaises(TypeError) as context:
            out.rename_columns('a')
        self.assertTrue(
            str(context.exception) ==
            "NiceTable.rename_columns(): expecting a list, got <class 'str'>",
            'rename_columns() expects a list of column names, not a string')

        with self.assertRaises(ValueError) as context:
            out.rename_columns(['a', 'b', 'c'])
        self.assertTrue(
            str(context.exception) == "NiceTable.rename_columns(): "
            "there are 5 columns, but got a list of 3 column names",
            "must provide names for all columns")

        out.rename_columns(['ID', 'Name', 'Type', 'Height(cm)', 'Weight(kg)'])
        expected_out = \
            '+-------+-------------+----------------+--------------+--------------+\n' + \
            '|  ID   |  Name       |  Type          |  Height(cm)  |  Weight(kg)  |\n' + \
            '+-------+-------------+----------------+--------------+--------------+\n' + \
            '|  001  |  Bulbasaur  |  Grass/Poison  |          70  |       6.901  |\n' + \
            '|  025  |  Pikachu    |  Electric      |          40  |       6.100  |\n' + \
            '|  150  |  Mewtwo     |  Psychic       |         200  |     122.000  |\n' + \
            '+-------+-------------+----------------+--------------+--------------+\n'
        self.assertEqual(expected_out, str(out),
                         'Correctly applying new column names')
Beispiel #10
0
    def test__constructor__data_only__list_of_mixed_dicts(self):
        out = NiceTable(data=[{
            "a": 1,
            "b": 2
        }, {
            "a": 11,
            "c": 33,
            "e": 55
        }, {
            "d": 999
        }, None, {
            "b": 4444,
            "d": 9999
        }],
                        value_none_string='---')
        expected_out = \
            '+-------+---------+-------+-------+---------+\n' + \
            '|  a    |  b      |  c    |  e    |  d      |\n' + \
            '+-------+---------+-------+-------+---------+\n' + \
            '|    1  |      2  |  ---  |  ---  |    ---  |\n' + \
            '|   11  |    ---  |   33  |   55  |    ---  |\n' + \
            '|  ---  |    ---  |  ---  |  ---  |    999  |\n' + \
            '|  ---  |    ---  |  ---  |  ---  |    ---  |\n' + \
            '|  ---  |   4444  |  ---  |  ---  |   9999  |\n' + \
            '+-------+---------+-------+-------+---------+\n'

        self.assertEqual(
            expected_out, str(out),
            'passing a list of non-uniform dicts correctly auto-generates column names'
        )
Beispiel #11
0
 def test__empty_table(self):
     t = NiceTable(col_names=['a', 'b'])
     expected_table = \
         '+-----+-----+\n' + \
         '|  a  |  b  |\n' + \
         '+-----+-----+\n' + \
         '+-----+-----+\n'
     self.assertEqual(expected_table, str(t), 'empty table printed nicely')
Beispiel #12
0
 def setUp(self):
     # all layout tests use the same data
     self.simple_tbl = NiceTable(
         col_names=['Name', 'Type', 'Height(cm)', 'Weight(kg)'])
     self.complex_tbl = NiceTable(
         col_names=['Name', None, 'Height\n(cm)', 'Weight\n(kg)'])
     for pokemon in json.loads(NiceTable.SAMPLE_JSON):
         self.simple_tbl.append([
             pokemon['name'], pokemon['type'], pokemon['height'],
             pokemon['weight']
         ])
         self.complex_tbl.append([
             pokemon['name'], pokemon['type'], pokemon['height'],
             pokemon['weight']
         ])
     self.complex_tbl.columns[1][1] = None
     self.complex_tbl.columns[2][1] = None
     self.complex_tbl.columns[1][0] = 'Grass\nPoison'
Beispiel #13
0
def run(config, verbose):
    """Validator of data that is queriable via SQL."""
    cfg = load(config)

    engine = db.create_engine(cfg['general']['sqla'])
    db_name = cfg['general']['db_name']

    validator_module = import_module('.validators', package='sqvid')

    n_failed = 0

    for table in cfg[db_name]:
        for column in cfg[db_name][table]:
            for val in cfg[db_name][table][column]:
                validator_name = val['validator']
                args = val.get('args')
                custom_column = val.get('custom_column')

                validator_fn = getattr(validator_module, validator_name)
                r, k, q = execute_validation(engine,
                                             table,
                                             column,
                                             validator_fn,
                                             args,
                                             custom_column=custom_column)

                col_names = val.get('report_columns', k)

                if custom_column:
                    column = "{} (customized as '{}')".format(
                        column, custom_column)

                if verbose:
                    print(QUERY_VERBOSE_STR.format(q))

                if len(r) == 0:
                    print("PASSED: Validation on [{}] {}.{} of {}{}".format(
                        db_name, table, column, validator_name,
                        '({})'.format(args) if args else ''))
                else:
                    print("FAILED: Validation on [{}] {}.{} of {}{}".format(
                        db_name,
                        table,
                        column,
                        validator_name,
                        '({})'.format(args) if args else '',
                    ))
                    print("Offending {} rows:".format(len(r)))
                    print(NiceTable(list(map(dict, r)), col_names=col_names))
                    n_failed += 1

    if n_failed > 0:
        sys.exit(1)
    else:
        sys.exit(0)
Beispiel #14
0
 def test__constructor__col_names_and_data__list_of_list(self):
     out1 = NiceTable(NiceTable.builtin_layouts(),
                      col_names=['Layout', 'Description'])
     out2 = NiceTable(col_names=['Layout', 'Description'])
     for layout in NiceTable.builtin_layouts():
         out2.append(layout)
     self.assertEqual(
         str(out1), str(out2),
         'initializing NiceTable with a list of lists is the same as appending each list in a loop'
     )
Beispiel #15
0
class Layouts(TestCase):
    # TODO add tests for each layout
    def setUp(self):
        # all layout tests use the same data
        self.simple_tbl = NiceTable(
            col_names=['Name', 'Type', 'Height(cm)', 'Weight(kg)'])
        self.complex_tbl = NiceTable(
            col_names=['Name', None, 'Height\n(cm)', 'Weight\n(kg)'])
        for pokemon in json.loads(NiceTable.SAMPLE_JSON):
            self.simple_tbl.append([
                pokemon['name'], pokemon['type'], pokemon['height'],
                pokemon['weight']
            ])
            self.complex_tbl.append([
                pokemon['name'], pokemon['type'], pokemon['height'],
                pokemon['weight']
            ])
        self.complex_tbl.columns[1][1] = None
        self.complex_tbl.columns[2][1] = None
        self.complex_tbl.columns[1][0] = 'Grass\nPoison'
        # print('simple\n' + str(self.simple_tbl))
        # print('complex\n' + str(self.complex_tbl))

    def test__get_column(self):
        self.assertEqual([6.901, 6.1, 122], self.simple_tbl.get_column(3),
                         'getting a column as a list of values')
        self.assertEqual([6.901, 6.1, 122],
                         self.simple_tbl.get_column('Weight(kg)'),
                         'getting a column as a list of values')
Beispiel #16
0
 def test__dot_annotation(self):
     expected_table = \
         '+------+-------+\n' + \
         '|  a   |  bbb  |\n' + \
         '+------+-------+\n' + \
         '|   1  |  yYy  |\n' + \
         '+------+-------+\n'
     self.assertEqual(
         expected_table,
         str(
             NiceTable(col_names=['a', 'bbb']).append([
                 1, None
             ]).set_col_options(1, none_string='yYy').set_col_options(
                 0, none_string='xXx')), 'using dot annotation should work')
Beispiel #17
0
 def test__constructor__col_names_and_data__mixed_list(self):
     out = NiceTable([[1, 2, 3], {
         'x': 1,
         'z': 2
     }, ("apple", "banana", "cherry")],
                     col_names=['a', 'b', 'x', 'y'])
     expected_out = \
         '+---------+----------+----------+--------+\n' \
         '|  a      |  b       |  x       |  y     |\n' \
         '+---------+----------+----------+--------+\n' \
         '|  1      |  2       |  3       |  None  |\n' \
         '|  None   |  None    |  1       |  None  |\n' \
         '|  apple  |  banana  |  cherry  |  None  |\n' \
         '+---------+----------+----------+--------+\n'
     self.assertEqual(
         expected_out, str(out),
         'initializing NiceTable with a mixed list of dicts/tuples/lists works'
     )
Beispiel #18
0
# Example: printing the list of builtin layouts
import json
from nicetable.nicetable import NiceTable
# from __future__ import annotations   # only for Python 3.7 and up?

out = NiceTable(['Layout', 'Description'])
for layout in NiceTable.builtin_layouts():
    out.append(layout)
print(out)

# Example: printing the sample JSON in two layouts
out = NiceTable(['Name', 'Type', 'Height(cm)', ' Weight(kg)'], layout='default')
for pokemon in json.loads(NiceTable.SAMPLE_JSON):
    out.append([pokemon['name'], pokemon['type'], pokemon['height'], pokemon['weight']])
print('-- default format --\n')
print(out)
out.layout = 'csv'
out.sep_vertical = '|'
print('-- CSV with a pipe separator --\n')
print(out)

# Example: printing all the formatting settings in md layout
out = NiceTable(['Setting', 'Type', 'Default', 'Description'], layout='md')
for setting in NiceTable.FORMATTING_SETTINGS:
    out.append(setting)
print(out)


# Example: custom layout
class MyNiceTable(NiceTable):
    def _layout_as_winter_columns(self) -> None:
Beispiel #19
0
class LayoutOptions(TestCase):
    """ Tests the effects of setting different layout options"""
    def setUp(
            self):  # TODO: maybe replace with a factory class like factory_boy
        # all layout options tests starts with the same table data:
        self.tbl = NiceTable(
            col_names=['Name', 'Type', 'Height(cm)', 'Weight(kg)'])
        for pokemon in json.loads(NiceTable.SAMPLE_JSON):
            self.tbl.append([
                pokemon['name'], pokemon['type'], pokemon['height'],
                pokemon['weight']
            ])

    def default_to_lines_cols(self) -> List[List[str]]:
        """Capture the test table as a string and return it as a list (by line) of list of string values."""
        lines = str(self.tbl).splitlines()[1:-1]  # remove top/bottom borders
        del lines[1]  # remove sepline
        sep = self.tbl.sep_vertical
        return list(line.strip(sep).split(sep) for line in lines)

    def default_to_cols_lines(self) -> List[List[str]]:
        """Capture the test table as a string and return it as a a list (by column) of list of string values."""
        return list(
            map(list,
                zip(*self.default_to_lines_cols())))  # "magic" transpose code

    def test__layout__lov(self):
        with self.assertRaises(ValueError) as context:
            self.tbl.layout = 'shiny_rainbow'
        self.assertTrue(
            str(context.exception).startswith(
                'Unknown table layout "shiny_rainbow", should be one of ['),
            'Specifying an unknown layout should raise with clear error')

    def test__header(self):
        lines_before = str(self.tbl).splitlines()
        self.tbl.header = False
        lines_after = str(self.tbl).splitlines()
        self.assertEqual(lines_before[2:], lines_after,
                         'Removing the header should remove two lines')

    def test__header_sepline(self):
        lines_before = str(self.tbl).splitlines()
        self.tbl.header_sepline = False
        lines_after = str(self.tbl).splitlines()
        del lines_before[2]
        self.assertEqual(
            lines_before, lines_after,
            'Removing the header sepline should remove one line when header was displayed'
        )

        self.tbl.header = False
        self.tbl.header_sepline = True
        lines_before = str(self.tbl).splitlines()
        self.tbl.header_sepline = False
        lines_after = str(self.tbl).splitlines()
        self.assertEqual(
            lines_before, lines_after,
            'Removing the header sepline should have no effect if header was not displayed'
        )

    def test__header_adjust__lov(self):
        with self.assertRaises(ValueError) as context:
            self.tbl.header_adjust = 'funky'
        self.assertTrue(
            str(context.exception).startswith(
                'Unknown adjust "funky", should be one of ['),
            'Specifying an unknown header adjustment should raise with clear error'
        )

    def test__header_adjust(self):
        self.tbl.header_adjust = 'center'
        header_line = str(self.tbl).splitlines()[1]
        self.assertEqual(
            '|     Name    |      Type      |  Height(cm)  |  Weight(kg)  |',
            header_line, 'Center-adjusted header')

        self.tbl.header_adjust = 'right'
        header_line = str(self.tbl).splitlines()[1]
        self.assertEqual(
            '|       Name  |          Type  |  Height(cm)  |  Weight(kg)  |',
            header_line, 'Right-adjusted header')

        self.tbl.header_adjust = 'left'
        header_line = str(self.tbl).splitlines()[1]
        self.assertEqual(
            '|  Name       |  Type          |  Height(cm)  |  Weight(kg)  |',
            header_line, 'Left-adjusted header')

    def test__borders(self):
        lines_before = str(self.tbl).splitlines()
        self.tbl.border_top = False
        lines_after = str(self.tbl).splitlines()
        del lines_before[0]
        self.assertEqual(lines_before, lines_after, 'Removed top border')

        self.tbl.border_bottom = False
        lines_after = str(self.tbl).splitlines()
        del lines_before[-1]
        self.assertEqual(lines_before, lines_after,
                         'Removed top + bottom borders')

        self.tbl.border_left = False
        lines_after = str(self.tbl).splitlines()
        lines_before = list(line[3:] for line in lines_before)
        self.assertEqual(lines_before, lines_after,
                         'removed top + bottom + left ("|  ") borders')

        self.tbl.border_right = False
        lines_after = str(self.tbl).splitlines()
        lines_before = list(line[:-3] for line in lines_before)
        self.assertEqual(
            lines_before, lines_after,
            'removed top + bottom + left + right ("  |") borders')

    def test__cell_adjust__lov(self):
        with self.assertRaises(ValueError) as context:
            self.tbl.cell_adjust = None
        self.assertTrue(
            str(context.exception).startswith(
                'Unknown adjust "None", should be one of ['),
            'Specifying an unknown cell adjustment should raise with clear error'
        )

    def test__cell_adjust(self):
        self.tbl.cell_adjust = 'left'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |    40        |    6.100     |',
            data_line, 'Left-adjusted data')

        self.tbl.cell_adjust = 'strict_left'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |  40          |  6.1         |',
            data_line,
            'Strict left-adjusted data - numbers are not auto-adjusted')

        self.tbl.cell_adjust = 'center'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|   Pikachu   |    Electric    |       40     |     6.100    |',
            data_line, 'Center-adjusted data')

        self.tbl.cell_adjust = 'strict_center'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|   Pikachu   |    Electric    |      40      |     6.1      |',
            data_line,
            'Strict center-adjusted data - numbers are not auto-adjusted')

        self.tbl.cell_adjust = 'right'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|    Pikachu  |      Electric  |          40  |       6.100  |',
            data_line, 'Right-adjusted data')

        self.tbl.cell_adjust = 'strict_right'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|    Pikachu  |      Electric  |          40  |         6.1  |',
            data_line,
            'Strict right-adjusted data - numbers are not auto-adjusted')

        self.tbl.cell_adjust = 'auto'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |          40  |       6.100  |',
            data_line,
            'auto-adjusted data (last column should be 6.100 due to other values in the column)'
        )

        self.tbl.cell_adjust = 'compact'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu  |  Electric  |  40  |  6.1  |', data_line,
            'compact data; (cell_spacing == 2) still applies, numbers appear in the output as-is'
        )

    def test__cell_spacing(self):
        self.tbl.cell_spacing = 1
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '| Pikachu   | Electric     |         40 |      6.100 |',
            data_line,
            'cell spacing test - should be one space (beyond the fixed column width'
        )

    def test__value_min_len(self):
        self.tbl.value_min_len = 5
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |          40  |       6.100  |',
            data_line, 'if value_min_len is too small, it has no effect')

        self.tbl.value_min_len = 13
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu        |  Electric       |             40  |          6.100  |',
            data_line,
            'long value_min_len - no column should be less than 13 characters')

    def test__value_newline_replace(self):
        self.tbl.columns[1][0] = 'Grass\nPoison'
        self.tbl.value_newline_replace = ' and '
        data_line = str(self.tbl).splitlines()[3]
        self.assertEqual(
            '|  Bulbasaur  |  Grass and Poison  |          70  |       6.901  |',
            data_line,
            'replace newline with a string. In this case, it made the column length to grow'
        )

        self.tbl.value_newline_replace = None
        data_line1 = str(self.tbl).splitlines()[3]
        data_line2 = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Bulbasaur  |  Grass     |          70  |       6.901  |',
            data_line1,
            'newline is splitting the value into two lines (line1)')
        self.assertEqual(
            '|             |  Poison    |              |              |',
            data_line2,
            'newline is splitting the value into two lines (line2)')

    # noinspection SpellCheckingInspection
    def test__value_max_len(self):
        self.tbl.value_max_len = 5
        self.tbl.value_too_long_policy = 'wrap'
        expected = \
            '+---------+---------+---------+---------+\n' + \
            '|  Name   |  Type   |  Heigh  |  Weigh  |\n' + \
            '|         |         |  t(cm)  |  t(kg)  |\n' + \
            '+---------+---------+---------+---------+\n' + \
            '|  Bulba  |  Grass  |     70  |    6.9  |\n' + \
            '|  saur   |  /Pois  |         |     01  |\n' + \
            '|         |  on     |         |         |\n' + \
            '|  Pikac  |  Elect  |     40  |    6.1  |\n' + \
            '|  hu     |  ric    |         |     00  |\n' + \
            '|  Mewtw  |  Psych  |    200  |  122.0  |\n' + \
            '|  o      |  ic     |         |     00  |\n' + \
            '+---------+---------+---------+---------+\n'
        self.assertEqual(
            expected, str(self.tbl),
            'wrapping column names and values every five characters')

        self.tbl.value_too_long_policy = 'truncate'
        expected = \
            '+---------+---------+---------+---------+\n' + \
            '|  Name   |  Type   |  Heigh  |  Weigh  |\n' + \
            '+---------+---------+---------+---------+\n' + \
            '|  Bulba  |  Grass  |     70  |    6.9  |\n' + \
            '|  Pikac  |  Elect  |     40  |    6.1  |\n' + \
            '|  Mewtw  |  Psych  |    200  |  122.0  |\n' + \
            '+---------+---------+---------+---------+\n'
        self.assertEqual(
            expected, str(self.tbl),
            'truncating long column names and values to five characters')

        self.tbl.value_max_len = 5
        self.tbl.value_too_long_policy = 'wrap'
        self.tbl.col_names[3] = 'a\n1234567\nabcdef\nXYZ\n'
        header_lines = '\n'.join(str(self.tbl).splitlines()[:9]) + '\n'
        expected_header = \
            '+---------+---------+---------+---------+\n' + \
            '|  Name   |  Type   |  Heigh  |  a      |\n' + \
            '|         |         |  t(cm)  |  12345  |\n' + \
            '|         |         |         |  67     |\n' + \
            '|         |         |         |  abcde  |\n' + \
            '|         |         |         |  f      |\n' + \
            '|         |         |         |  XYZ    |\n' + \
            '|         |         |         |         |\n' + \
            '+---------+---------+---------+---------+\n'
        self.assertEqual(
            expected_header, header_lines,
            'combining multiple newlines in the header with max_value_len and wrapping'
        )

    def test__value_too_long_policy(self):
        pass  # covered by test__value_max_len

    def test__value_none_string(self):
        self.tbl.col_names[1] = None
        self.tbl.columns[1][1] = None
        self.tbl.columns[2][1] = None

        header_line = str(self.tbl).splitlines()[1]
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Name       |  None          |  Height(cm)  |  Weight(kg)  |',
            header_line,
            'None value for a field name should become self.value_none_string')
        self.assertEqual(
            '|  Pikachu    |  None          |        None  |       6.100  |',
            data_line,
            'None value in data should become self.value_none_string, aligned by column setting'
        )

        self.tbl.col_names[1] = 'Type'
        self.tbl.value_none_string = '-- NO VALUE NO VALUE NO VALUE --'
        self.assertEqual(
            min(len(line) for line in str(self.tbl).splitlines()),
            max(len(line) for line in str(self.tbl).splitlines()),
            'all lines should be the same length, after dynamically changing NULL string to a long one'
        )

    def test__sep_vertical(self):
        self.tbl.sep_vertical = 'oOo'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            'oOo  Pikachu    oOo  Electric      oOo          40  oOo       6.100  oOo',
            data_line, 'value sep should be oOo')

    def test__value_escape_type__lov(self):
        with self.assertRaises(ValueError) as context:
            self.tbl.value_escape_type = 'escape'
        self.assertTrue(
            str(context.exception).startswith(
                'Unknown value escape type "escape", should be one of ['),
            'Specifying an unknown value escape type should raise with clear error'
        )

    def test__value_escape_type(self):
        self.tbl.sep_vertical = '/'  # value "Grass/Poison" in cell [1][1] now includes the value sep "/" in it

        self.tbl.value_escape_type = 'remove'
        value = self.default_to_cols_lines()[1][1]
        self.assertEqual(
            'GrassPoison', value.strip(),
            'handling sep_vertical character in value by removing it')

        self.tbl.value_escape_type = 'replace'
        self.tbl.value_escape_char = '+'
        value = self.default_to_cols_lines()[1][1]
        self.assertEqual(
            'Grass+Poison', value.strip(),
            'handling sep_vertical character in value by replacing it')

        self.tbl.value_escape_type = 'prefix'
        self.tbl.value_escape_char = '\\'
        data_line = str(self.tbl).splitlines()[3]
        self.assertEqual(
            '/  Bulbasaur  /  Grass\/Poison  /          70  /       6.901  /',
            data_line,
            'handling sep_vertical character in value by prefixing it')

        self.tbl.value_escape_type = 'ignore'
        data_line = str(self.tbl).splitlines()[3]
        self.assertEqual(
            '/  Bulbasaur  /  Grass/Poison  /          70  /       6.901  /',
            data_line, 'ignoring the sep_vertical character in the value')

    def test__value_escape_char(self):
        pass  # covered by test__value_escape_type

    def test__sep_cross(self):
        self.tbl.sep_cross = '/'
        data_line = str(self.tbl).splitlines()[0]
        self.assertEqual(
            '/-------------/----------------/--------------/--------------/',
            data_line, 'set sepline separator to /')

    def test__sep_horizontal(self):
        self.tbl.sep_horizontal = '*'
        data_line = str(self.tbl).splitlines()[0]
        self.assertEqual(
            '+*************+****************+**************+**************+',
            data_line, 'set sepline character to *')

    def test__value_func__exception(self):
        with self.assertRaises(TypeError) as context:
            self.tbl.value_func = 'not a function'
        self.assertEqual(
            "value_func should be a function, got 'not a function' of type <class 'str'>",
            str(context.exception), 'value_func should be a function')

    def test__value_func(self):
        self.tbl.value_func = lambda x: 5 if isinstance(x, numbers.Number
                                                        ) else x.swapcase()
        data_line = str(self.tbl).splitlines()[4]
        # noinspection SpellCheckingInspection
        self.assertEqual(
            '|  pIKACHU    |  eLECTRIC      |           5  |       5.000  |',
            data_line, 'applies a lambda to all columns')
        self.tbl.value_func = None
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |          40  |       6.100  |',
            data_line, "resetting value_func to make the output normal again")

    def test__set_col_options__exceptions(self):
        with self.assertRaises(IndexError) as context:
            self.tbl.set_col_options('my col', adjust='center')
        self.assertTrue(
            str(context.exception).startswith(
                'NiceTable.set_col_options(): got col name "my col", expecting one of'
            ),
            'when first param of set_col_options is a str, it must be a valid column name'
        )

        with self.assertRaises(IndexError) as context:
            self.tbl.set_col_options(77, adjust='center')
        self.assertEqual(
            'NiceTable.set_col_options(): got col index 77, expecting index in the range of "0..3"',
            str(context.exception),
            'when first param of set_col_options is a int, it must be a valid column number'
        )

        with self.assertRaises(TypeError) as context:
            self.tbl.set_col_options(None, adjust='right')
        self.assertEqual(
            "NiceTable.set_col_options(): " +
            "first parameter should be str or int (column name or position), got <class 'NoneType'>",
            str(context.exception),
            'first param of set_col_options must be int or str')

    def test__set_col_options__adjust(self):
        with self.assertRaises(ValueError) as context:
            self.tbl.set_col_options(0, adjust='nothing')
        self.assertTrue(
            str(context.exception).startswith(
                'NiceTable.set_col_options(): got adjust value "nothing", expecting one of '
            ),
            'Specifying an unknown column adjustment should raise with clear error'
        )

        self.tbl.set_col_options(3, adjust='left')
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Pikachu    |  Electric      |          40  |    6.100     |',
            data_line, 'Left-adjusted forth column')

        self.tbl.cell_adjust = 'right'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|    Pikachu  |      Electric  |          40  |    6.100     |',
            data_line,
            'now, all columns should be right adjusted except the forth, due to its column-level settings'
        )

    # noinspection SpellCheckingInspection
    def test__set_col_options__max_len(self):
        self.tbl.value_max_len = 7
        self.tbl.set_col_options('Type', max_len=9)
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  Bulbasa  |  Grass/Poi  |       70  |    6.901  |', data_line,
            'override the max column length for the second column')

    def test__set_col_options__newline_replace(self):
        self.tbl.columns[1][0] = 'Grass\nPoison'
        self.tbl.value_newline_replace = ' and '
        self.tbl.set_col_options('Type', newline_replace=' or ')
        data_line = str(self.tbl).splitlines()[3]
        self.assertEqual(
            '|  Bulbasaur  |  Grass or Poison  |          70  |       6.901  |',
            data_line,
            ' newline replace for second column should be from column-level setting'
        )

    def test__set_col_options__none_string(self):
        self.tbl.col_names[1] = None
        self.tbl.columns[1][1] = None
        self.tbl.columns[2][1] = None
        self.tbl.set_col_options(1, none_string='<oops>')
        header_line = str(self.tbl).splitlines()[1]
        data_line = str(self.tbl).splitlines()[4]

        self.assertEqual(
            '|  Name       |  <oops>        |  Height(cm)  |  Weight(kg)  |',
            header_line, 'column-level none string in header')
        self.assertEqual(
            '|  Pikachu    |  <oops>        |        None  |       6.100  |',
            data_line,
            'column-level none string for column affects only second column, not third one'
        )

    def test__set_col_options__func(self):
        with self.assertRaises(TypeError) as context:
            self.tbl.set_col_options(0, func='not a function')
        self.assertEqual(
            "NiceTable.set_col_options(): " +
            "func parameter should be a function, got <class 'str'>",
            str(context.exception),
            'func param of set_col_options must be a function')

        self.tbl.set_col_options(0, func=lambda x: x.upper())
        self.tbl.set_col_options('Type',
                                 func=lambda x: x.lower()
                                 if x != 'Electric' else None)

        data_cols = self.default_to_cols_lines()
        self.assertEqual(
            ['BULBASAUR', 'PIKACHU', 'MEWTWO'],
            list(value.strip() for value in data_cols[0][1:]),
            'applying this function should result in uppercase values')
        self.assertEqual(
            ['grass/poison', 'None', 'psychic'],
            list(value.strip() for value in data_cols[1][1:]),
            'applying this function should result in lowercase / None values')

        self.tbl.value_func = lambda x: 'aaa'
        data_line = str(self.tbl).splitlines()[4]
        self.assertEqual(
            '|  PIKACHU    |  None          |  aaa         |  aaa         |',
            data_line,
            'value_func should only apply to columns without column function')