コード例 #1
0
def live(ctx: click.Context, **args: str) -> None:
    """
    Show live departures and arrivals for a train station.

    \b
    Examples:
    locomotive live "Paris Montparnasse"
    """
    stations = ctx.obj["stations"]
    station = stations.find_or_raise(args["station"])

    client = Client(stations)

    dep_gs = None
    arr_gs = None
    if args["color"]:
        dep_gs = tf.AlternatingRowGrid(bg(25), bg(18))
        arr_gs = tf.AlternatingRowGrid(bg(34), bg(28))

    departures = client.board_request(BoardRequest(station, "departure"))
    cols = ["Destination", "Number", "Time", "Delay", "Platform"]
    click.echo(tf.generate_table(get_rows(departures), cols,
                                 grid_style=dep_gs))

    arrivals = client.board_request(BoardRequest(station, "arrival"))
    cols = ["Origin", "Number", "Time", "Delay", "Platform"]
    click.echo(tf.generate_table(get_rows(arrivals), cols, grid_style=arr_gs))
コード例 #2
0
ファイル: cmd2_tables.py プロジェクト: ra2003/tableformatter
    def ptable(self, rows, columns, grid_args, row_stylist):
        """Format tabular data for pretty-printing as a fixed-width table and then display it using a pager.

        :param rows: required argument - can be a list-of-lists (or another iterable of iterables), a two-dimensional
                             NumPy array, or an Iterable of non-iterable objects
        :param columns: column headers and formatting options per column
        :param grid_args: argparse arguments for formatting the grid
        :param row_stylist: function to determine how each row gets styled
        """
        if grid_args.color:
            grid = tf.AlternatingRowGrid(BACK_PRI, BACK_ALT)
        elif grid_args.fancy:
            grid = tf.FancyGrid()
        elif grid_args.sparse:
            grid = tf.SparseGrid()
        else:
            grid = None

        transpose = False
        if grid_args.transpose:
            transpose = True

        formatted_table = tf.generate_table(rows=rows, columns=columns, grid_style=grid, row_tagger=row_stylist,
                                            transpose=transpose)
        self.ppaged(formatted_table, chop=True)
コード例 #3
0
def test_namedtuple_rows():
    expected = '''
╔══════════════════════╤════════╤═════╤═══════╤════════════╗
║                      │        │ Num │       │            ║
║ First                │ Second │ 1   │ Num 2 │ Multiplied ║
╠══════════════════════╪════════╪═════╪═══════╪════════════╣
║ RLonger text that     │ RA2     │ R5   │ R56    │ R280        ║
║ Rwill trigger the     │        │     │       │            ║
║ Rcolumn wrapping      │        │     │       │            ║
║ GB1                   │ GB2     │ G23  │ G8     │ G184        ║
║                      │ GB2     │     │       │            ║
║                      │ GB2     │     │       │            ║
║ C1                   │ C2     │ 4   │ 9     │ 36         ║
║ D1                   │ D2     │ 7   │ 5     │ 35         ║
╚══════════════════════╧════════╧═════╧═══════╧════════════╝
'''.lstrip('\n')
    rows = [
        tf.Row(NamedTupleRow(
            'Longer text that will trigger the column wrapping', 'A2', 5, 56),
               text_color='R'),
        tf.Row(NamedTupleRow('B1', 'B2\nB2\nB2', 23, 8), text_color='G'),
        NamedTupleRow('C1', 'C2', 4, 9),
        NamedTupleRow('D1', 'D2', 7, 5)
    ]

    columns = (tf.Column('First', width=20,
                         attrib='field1'), tf.Column('Second',
                                                     attrib='field2'),
               tf.Column('Num 1', width=3,
                         attrib='field3'), tf.Column('Num 2', attrib='field4'),
               tf.Column('Multiplied', obj_formatter=multiply_named_tuple))
    table = tf.generate_table(rows, columns)
    assert table == expected
コード例 #4
0
def test_tuple_rows():
    expected = '''
╔══════════════════════╤════════╤═══════╤═══════════╤════════════╗
║ First                │ Second │ Num 1 │ Num 2     │ Multiplied ║
╠══════════════════════╪════════╪═══════╪═══════════╪════════════╣
║ RLonger text that wil │ RA2     │ R5     │ RFifty-six │ R280        ║
║ GB1                   │ GB2     │ G23    │ GEight     │ G184        ║
║                      │ GB2     │       │           │            ║
║                      │ GB2     │       │           │            ║
║ C1                   │ C2     │ 4     │ Nine      │ 36         ║
║ D1                   │ D2     │ 7     │ Five      │ 35         ║
╚══════════════════════╧════════╧═══════╧═══════════╧════════════╝
'''.lstrip('\n')

    rows = [
        tf.Row('Longer text that will trigger the column wrapping',
               'A2',
               5,
               56,
               None,
               text_color='R'),
        tf.Row('B1', 'B2\nB2\nB2', 23, 8, None, text_color='G'),
        ('C1', 'C2', 4, 9, None), ('D1', 'D2', 7, 5, None)
    ]

    columns = (tf.Column('First',
                         width=20,
                         wrap_mode=tf.WrapMode.TRUNCATE_HARD),
               tf.Column('Second'), tf.Column('Num 1'),
               tf.Column('Num 2', formatter=int2word),
               tf.Column('Multiplied', obj_formatter=multiply_tuple))

    table = tf.generate_table(rows, columns)

    assert table == expected
コード例 #5
0
def test_fmt_tuple_rows():
    expected = '''
╔═══════════╤═══════════════════╤═══════╤═══════════╤════════════╗
║ First     │ Second            │ Num 1 │ Num 2     │ Multiplied ║
╠═══════════╪═══════════════════╪═══════╪═══════════╪════════════╣
║           │                   │ 17    │ Four      │ 68         ║
║ 123.00  B │               123 │ 5     │ Fifty-six │ 280        ║
║ 123.00  B │               123 │ 5     │ Fifty-six │ 280        ║
║  12.06 KB │            12,345 │ 23    │ Eight     │ 184        ║
║  11.77 MB │        12,345,678 │ 4     │ Nine      │ 36         ║
║   1.15 GB │     1,234,567,890 │ 7     │ Five      │ 35         ║
║   1.12 TB │ 1,234,567,890,123 │ 7     │ Five      │ 35         ║
╚═══════════╧═══════════════════╧═══════╧═══════════╧════════════╝
'''.lstrip('\n')

    rows = [(None, None, 17, 4, None), ('123', '123', 5, 56, None),
            (123, 123, 5, 56, None), (12345, 12345, 23, 8, None),
            (12345678, 12345678, 4, 9, None),
            (1234567890, 1234567890, 7, 5, None),
            (1234567890123, 1234567890123, 7, 5, None)]

    columns = (tf.Column('First',
                         width=20,
                         formatter=tf.FormatBytes(),
                         cell_halign=tf.ColumnAlignment.AlignRight),
               tf.Column('Second',
                         formatter=tf.FormatCommas(),
                         cell_halign=tf.ColumnAlignment.AlignRight),
               tf.Column('Num 1'), tf.Column('Num 2', formatter=int2word),
               tf.Column('Multiplied', obj_formatter=multiply_tuple))

    table = tf.generate_table(rows, columns)

    assert table == expected
コード例 #6
0
def test_iterable_of_non_iterable_objects():
    rows = [MyRowObject(1, 2, 3, 4), MyRowObject(5, 6, 7, 8)]
    columns = (tf.Column('col1',
                         attrib='field1'), tf.Column('col2', attrib='field2'),
               tf.Column('col3', attrib='get_field3'),
               tf.Column('col4', attrib='field4'))
    table = tf.generate_table(rows, columns)
    assert table == EXPECTED_WITH_HEADERS
コード例 #7
0
ファイル: fw_utils.py プロジェクト: andrewpeck/0xbefe
def befe_print_mgt_status(txrx):
    mgts = befe_get_all_mgts(txrx)
    cols = mgt_get_status_labels()
    rows = []
    for mgt in mgts:
        status = mgt.get_status()
        status[0] = txrx.name + " " + status[0]
        rows.append(status)
    print(tf.generate_table(rows, cols, grid_style=DEFAULT_TABLE_GRID_STYLE))
コード例 #8
0
def test_iterable_of_dicts():
    d1 = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}
    d2 = {5: 'e', 6: 'f', 7: 'g', 8: 'h'}
    iterable_of_dicts = [
        OrderedDict(sorted(d1.items(), key=lambda t: t[0])),
        OrderedDict(sorted(d2.items(), key=lambda t: t[0]))
    ]
    table = tf.generate_table(iterable_of_dicts)
    assert table == EXPECTED_BASIC
コード例 #9
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_basic_sparse_grid(rows):
    expected = '''
 A1  A2  A3  A4 
 B1  B2  B3  B4 
     B2         
     B2         
 C1  C2  C3  C4 
 D1  D2  D3  D4 \n                \n'''.lstrip('\n')
    table = tf.generate_table(rows, grid_style=tf.SparseGrid())
    assert table == expected
コード例 #10
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_object_table_sparse_grid(obj_rows, obj_cols):
    expected = '''
 A1  A2  A3  A4 
 B1  B2  B3  B4 
     B2         
     B2         
 C1  C2  C3  C4 
 D1  D2  D3  D4 \n                \n'''.lstrip('\n')
    table = tf.generate_table(obj_rows, obj_cols, grid_style=tf.SparseGrid())
    assert table == expected
コード例 #11
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_table_with_header_transposed_sparse(rows, cols):
    expected = '''
 Col1  A1  B1  C1  D1 
 Col2  A2  B2  C2  D2 
           B2         
           B2         
 Col3  A3  B3  C3  D3 
 Col4  A4  B4  C4  D4 \n                      \n'''.lstrip('\n')
    table = tf.generate_table(rows, cols, grid_style=tf.SparseGrid(), transpose=True)
    assert table == expected
コード例 #12
0
ファイル: main.py プロジェクト: diptangsu/pyenvcomp
def envs_display(env1_path, env2_path, heading, diff_or_similar_list,
                 similar: bool):
    """Displays the table of similar or different module versions."""
    color = Colors.OKGREEN if similar else Colors.WARNING
    title = "SAME MODULE VERSIONS " if similar else "DIFFERENT MODULE VERSIONS "

    print(color + title + Colors.END)
    print(f'{env1_path.split(os.sep)[-1]} - {env1_path}')
    print(f'{env2_path.split(os.sep)[-1]} - {env2_path}')
    print(tf.generate_table(diff_or_similar_list, heading))
コード例 #13
0
ファイル: fw_utils.py プロジェクト: andrewpeck/0xbefe
def befe_print_link_status(links, txrx=None):
    if len(links) == 0:
        return
    cols = links[0].get_status_labels() if txrx is None else links[0].get_txrx_status_labels(txrx)
    rows = []
    for link in links:
        status = link.get_status() if txrx is None else link.get_txrx_status(txrx)
        rows.append(status)

    print(tf.generate_table(rows, cols, grid_style=DEFAULT_TABLE_GRID_STYLE)) # FULL_TABLE_GRID_STYLE
コード例 #14
0
def edit(**kwargs: str) -> None:
    # FIXME: Compare plan afterwards before submitting.
    api = priolib.client.APIClient(SERVER_ADDR)
    try:
        plan = api.get_plan()
    except priolib.client.ConnectionError:
        print(f'Unable to connect to server {SERVER_ADDR}.')
        return
    except priolib.client.APIError as exc:
        print(exc.message)
        print(exc.details)
        return

    opts = parse_display_options('STATUS,TASK,ID')
    tasks = \
        plan.done + \
        plan.today + \
        plan.todo + \
        plan.blocked + \
        plan.later
    cols = task_col_obj(opts)
    rows = [TaskRowObject.From_task(t) for t in tasks]
    table = tf.generate_table(rows, cols, grid_style=EditGrid())
    message = click.edit(table)
    if message is None:
        return
    p = priolib.model.Plan([], [], [], [], [])
    for line in message.split('\n')[:-2]:
        tokens = line.split()
        print(tokens)
        status = tokens[0]
        t = priolib.model.Task(
            status=status,
            id_=tokens[-1],
        )
        if status == 'Done':
            p.done.append(t)
        if status == 'Today':
            p.today.append(t)
        if status == 'Todo':
            p.todo.append(t)
        if status == 'Blocked':
            p.blocked.append(t)
        if status == 'Later':
            p.later.append(t)

    try:
        api.update_plan(p)
    except priolib.client.ConnectionError:
        print(f'Unable to connect to server {SERVER_ADDR}.')
        return
    except priolib.client.APIError as exc:
        print(exc.message)
        print(exc.details)
        return
コード例 #15
0
ファイル: summary.py プロジェクト: b3n4kh/icinga-summary
def main(host: str, basicauth: bool):
    services = get_object(object='services', basicauth=basicauth, host=host)
    rows = []

    for service, data in services.items():
        rows.append(IcingaStatus(service, data['state'], data['output']))

    print(
        generate_table(rows,
                       columns,
                       grid_style=FancyGrid(),
                       row_tagger=status_color))
コード例 #16
0
ファイル: prbs_old.py プロジェクト: andrewpeck/0xbefe
def prbs_status():
    cols = ["Link", "RX Usage", "RX Type", "RX MGT", "RX PRBS Mode", "TX PRBS Mode", "PRBS Error Count"]
    rows = []
    links = befe_get_all_links()
    for link in links:
        rx_mgt = link.get_mgt(MgtTxRx.RX)
        tx_mgt = link.get_mgt(MgtTxRx.TX)
        prbs_err_cnt = link.get_prbs_err_cnt()
        row = [link.idx, link.rx_usage, rx_mgt.type, rx_mgt.idx, rx_mgt.get_prbs_mode(), tx_mgt.get_prbs_mode(), prbs_err_cnt]
        rows.append(row)

    print(tf.generate_table(rows, cols, grid_style=DEFAULT_TABLE_GRID_STYLE))
コード例 #17
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_basic_table(rows):
    expected = '''
╔════╤════╤════╤════╗
║ A1 │ A2 │ A3 │ A4 ║
║ B1 │ B2 │ B3 │ B4 ║
║    │ B2 │    │    ║
║    │ B2 │    │    ║
║ C1 │ C2 │ C3 │ C4 ║
║ D1 │ D2 │ D3 │ D4 ║
╚════╧════╧════╧════╝
'''.lstrip('\n')
    table = tf.generate_table(rows)
    assert table == expected
コード例 #18
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_basic_transposed(rows):
    expected = '''
╔════╤════╤════╤════╗
║ A1 │ B1 │ C1 │ D1 ║
║ A2 │ B2 │ C2 │ D2 ║
║    │ B2 │    │    ║
║    │ B2 │    │    ║
║ A3 │ B3 │ C3 │ D3 ║
║ A4 │ B4 │ C4 │ D4 ║
╚════╧════╧════╧════╝
'''.lstrip('\n')
    table = tf.generate_table(rows, transpose=True)
    assert table == expected
コード例 #19
0
    def test_numpy_record_array():
        np_rec_array = np.rec.array([(1, 2., 'Hello'), (2, 3., "World")],
                                    dtype=[('foo', 'i4'), ('bar', 'f4'),
                                           ('baz', 'U10')])
        table = tf.generate_table(np_rec_array)
        expected = '''
╔═════╤═════╤═══════╗
║ foo │ bar │ baz   ║
╠═════╪═════╪═══════╣
║ 1   │ 2.0 │ Hello ║
║ 2   │ 3.0 │ World ║
╚═════╧═════╧═══════╝
'''.lstrip('\n')
        assert table == expected
コード例 #20
0
ファイル: fitresult.py プロジェクト: mozgit/zfit
 def __str__(self):
     string = Style.BRIGHT + f'FitResult' + Style.NORMAL + f' of\n{self.loss} \nwith\n{self.minimizer}\n'
     string += tafo.generate_table(
         [[
             color_on_bool(self.converged),
             format_value(self.edm, highprec=False),
             format_value(self.fmin)
         ]],
         ['converged', 'edm', 'min value'],
         # grid_style=tafo.SparseGrid()
     )
     string += Style.BRIGHT + "Parameters\n"
     string += str(self.params)
     return string
コード例 #21
0
 def get_scratched_ticket(self):
     new_tkt = self.ticket
     self.ticket = [
         strike_through(x)
         if x in list(set(self.ticket) -
                      set(self.remaining_ticket)) else "\033[1;31m" +
         str(x) + "\033[1;31m" for x in new_tkt
     ]
     np_ticket = np.array(self.ticket)
     shape = constants.NUMPY_SHAPE[len(self.ticket)]
     np_ticket = np_ticket.reshape(shape[0], shape[1])
     print(
         tf.generate_table(np_ticket,
                           grid_style=tf.AlternatingRowGrid(
                               BACK_GREEN, BACK_BLUE)))
コード例 #22
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_basic_fancy_grid(rows):
    expected = '''
╔════╤════╤════╤════╗
║ A1 │ A2 │ A3 │ A4 ║
╟────┼────┼────┼────╢
║ B1 │ B2 │ B3 │ B4 ║
║    │ B2 │    │    ║
║    │ B2 │    │    ║
╟────┼────┼────┼────╢
║ C1 │ C2 │ C3 │ C4 ║
╟────┼────┼────┼────╢
║ D1 │ D2 │ D3 │ D4 ║
╚════╧════╧════╧════╝
'''.lstrip('\n')
    table = tf.generate_table(rows, grid_style=tf.FancyGrid())
    assert table == expected
コード例 #23
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_table_with_header_transposed_fancy(rows, cols):
    expected = '''
╔══════╦════╤════╤════╤════╗
║ Col1 ║ A1 │ B1 │ C1 │ D1 ║
╟──────╫────┼────┼────┼────╢
║ Col2 ║ A2 │ B2 │ C2 │ D2 ║
║      ║    │ B2 │    │    ║
║      ║    │ B2 │    │    ║
╟──────╫────┼────┼────┼────╢
║ Col3 ║ A3 │ B3 │ C3 │ D3 ║
╟──────╫────┼────┼────┼────╢
║ Col4 ║ A4 │ B4 │ C4 │ D4 ║
╚══════╩════╧════╧════╧════╝
'''.lstrip('\n')
    table = tf.generate_table(rows, cols, grid_style=tf.FancyGrid(), transpose=True)
    assert table == expected
コード例 #24
0
ファイル: commands.py プロジェクト: w7374520/JSShell
    def _commands_plugin_show_all(self, limit: int) -> None:
        """ Shows all commands """

        max_cmd_w, max_out_w = available_max_width_on_screen_for_commands(
            self.selected_client.max_commands_width,
            self.selected_client.max_outputs_width)

        table = tf.generate_table(
            grid_style=tf.FancyGrid(),
            columns=['ID', 'Status', 'Created On', 'Command', 'Output'],
            rows=[
                cmd.to_table_list(max_cmd_w, max_out_w)
                for cmd in self.selected_client.reversed_commands[:limit]
            ])

        self.ppaged(table, chop=True)
コード例 #25
0
ファイル: decrypt.py プロジェクト: djeley/ssc-decryptor
def _print_alphabets(alphabets_map: dict) -> None:
    columns = ['Cypher alphabet']
    columns.extend(alphabets_map.keys())
    rows = list()
    for value in itertools.zip_longest(*alphabets_map.values(),
                                       fillvalue=None):
        value_list = list(value)
        if len(rows) == 0:
            value_list.insert(0, 'Plain alphabet')
        else:
            value_list.insert(0, None)

        rows.append(tuple(value_list))

    #print("")
    # tableformatter doesn't appear to prevent further bold formatting.
    logger.info(tableformatter.generate_table(rows, columns) + "\033[0m")
コード例 #26
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_object_table_fancy_grid(obj_rows, obj_cols):
    expected = '''
╔══════╤══════╤══════╤══════╗
║ Col1 │ Col2 │ Col3 │ Col4 ║
╠══════╪══════╪══════╪══════╣
║ A1   │ A2   │ A3   │ A4   ║
╟──────┼──────┼──────┼──────╢
║ B1   │ B2   │ B3   │ B4   ║
║      │ B2   │      │      ║
║      │ B2   │      │      ║
╟──────┼──────┼──────┼──────╢
║ C1   │ C2   │ C3   │ C4   ║
╟──────┼──────┼──────┼──────╢
║ D1   │ D2   │ D3   │ D4   ║
╚══════╧══════╧══════╧══════╝
'''.lstrip('\n')
    table = tf.generate_table(obj_rows, obj_cols, grid_style=tf.FancyGrid())
    assert table == expected
コード例 #27
0
ファイル: test_simple.py プロジェクト: ra2003/tableformatter
def test_object_table_columns_rearranged(obj_rows):
    cols2 = (tf.Column('Col1', attrib='field3'),
             tf.Column('Col2', attrib='field2'),
             tf.Column('Col3', attrib='field1'),
             tf.Column('Col4', attrib='field4'))
    expected = '''
╔══════╤══════╤══════╤══════╗
║ Col1 │ Col2 │ Col3 │ Col4 ║
╠══════╪══════╪══════╪══════╣
║      │ A2   │ A1   │ A4   ║
║      │ B2   │ B1   │ B4   ║
║      │ B2   │      │      ║
║      │ B2   │      │      ║
║      │ C2   │ C1   │ C4   ║
║      │ D2   │ D1   │ D4   ║
╚══════╧══════╧══════╧══════╝
'''.lstrip('\n')
    table = tf.generate_table(obj_rows, cols2)
    assert table == expected
コード例 #28
0
ファイル: test_columns.py プロジェクト: ra2003/tableformatter
def test_truncate_middle_cell_align_bottom(obj_rows):
    columns = (tf.Column('First', width=20, attrib='field1', wrap_mode=tf.WrapMode.TRUNCATE_MIDDLE),
               tf.Column('Second', attrib='field2'),
               tf.Column('Num 1', attrib='get_field3'),
               tf.Column('Num 2', attrib='field4', cell_valign=tf.ColumnAlignment.AlignBottom),
               tf.Column('Multiplied', attrib=None, obj_formatter=multiply))
    table = tf.generate_table(obj_rows, columns)
    expected = '''
╔══════════════════════╤════════╤═══════╤═══════╤════════════╗
║ First                │ Second │ Num 1 │ Num 2 │ Multiplied ║
╠══════════════════════╪════════╪═══════╪═══════╪════════════╣
║ Longer t … wrapping  │ A2     │ 5     │ 56    │ 280        ║
║ B1                   │ B2     │ 23    │       │ 184        ║
║                      │ B2     │       │       │            ║
║                      │ B2     │       │ 8     │            ║
║ C1                   │ C2     │ 4     │ 9     │ 36         ║
║ D1                   │ D2     │ 7     │ 5     │ 35         ║
╚══════════════════════╧════════╧═══════╧═══════╧════════════╝
'''.lstrip('\n')
    assert table == expected
コード例 #29
0
ファイル: test_columns.py プロジェクト: ra2003/tableformatter
def test_truncate_front_custom_padding_cell_align_right(obj_rows):
    columns = (tf.Column('First', width=20, attrib='field1', wrap_mode=tf.WrapMode.TRUNCATE_FRONT),
               tf.Column('Second', attrib='field2', cell_padding=5, cell_halign=tf.ColumnAlignment.AlignRight),
               tf.Column('Num 1', attrib='get_field3'),
               tf.Column('Num 2', attrib='field4'),
               tf.Column('Multiplied', attrib=None, obj_formatter=multiply))
    table = tf.generate_table(obj_rows, columns)
    expected = '''
╔══════════════════════╤════════════════╤═══════╤═══════╤════════════╗
║ First                │     Second     │ Num 1 │ Num 2 │ Multiplied ║
╠══════════════════════╪════════════════╪═══════╪═══════╪════════════╣
║ …the column wrapping │         A2     │ 5     │ 56    │ 280        ║
║ B1                   │         B2     │ 23    │ 8     │ 184        ║
║                      │         B2     │       │       │            ║
║                      │         B2     │       │       │            ║
║ C1                   │         C2     │ 4     │ 9     │ 36         ║
║ D1                   │         D2     │ 7     │ 5     │ 35         ║
╚══════════════════════╧════════════════╧═══════╧═══════╧════════════╝
'''.lstrip('\n')
    assert table == expected
コード例 #30
0
ファイル: test_columns.py プロジェクト: ra2003/tableformatter
def test_truncate_hard_field_formatter(obj_rows):
    columns = (tf.Column('First', width=20, attrib='field1', wrap_mode=tf.WrapMode.TRUNCATE_HARD),
               tf.Column('Second', attrib='field2'),
               tf.Column('Num 1', attrib='get_field3'),
               tf.Column('Num 2', attrib='field4', formatter=int2word),
               tf.Column('Multiplied', attrib=None, obj_formatter=multiply))
    table = tf.generate_table(obj_rows, columns)
    expected = '''
╔══════════════════════╤════════╤═══════╤═══════════╤════════════╗
║ First                │ Second │ Num 1 │ Num 2     │ Multiplied ║
╠══════════════════════╪════════╪═══════╪═══════════╪════════════╣
║ Longer text that wil │ A2     │ 5     │ Fifty-six │ 280        ║
║ B1                   │ B2     │ 23    │ Eight     │ 184        ║
║                      │ B2     │       │           │            ║
║                      │ B2     │       │           │            ║
║ C1                   │ C2     │ 4     │ Nine      │ 36         ║
║ D1                   │ D2     │ 7     │ Five      │ 35         ║
╚══════════════════════╧════════╧═══════╧═══════════╧════════════╝
'''.lstrip('\n')
    assert table == expected