|
| 1 | +#!/usr/bin/env python |
| 2 | +# coding=utf-8 |
| 3 | +"""A simple example demonstrating the following: |
| 4 | + 1) How to integrate tableformatter into an interactive command-line application using the cmd2 module |
| 5 | + 2) How to display table output using a pager |
| 6 | +
|
| 7 | +Run this applicaiton with: |
| 8 | + python cmd2_tables.py |
| 9 | +
|
| 10 | +Then type "help" to get a help menu. The two custom commands implemented are: |
| 11 | + - table |
| 12 | + - object_table |
| 13 | +Each of these commands takes various flags as arguments which can alter the grid style. |
| 14 | +
|
| 15 | +To get help on one of these commands do: |
| 16 | + - help table |
| 17 | +
|
| 18 | +NOTE: IF the table does not entirely fit within the screen of your terminal, then it will be displayed using a pager. |
| 19 | +You can use the arrow keys (left, right, up, and down) to scroll around the table as well as the PageUp/PageDown keys. |
| 20 | +You can quit out of the pager by typing "q". You can also search for text within the pager using "/". |
| 21 | +
|
| 22 | +WARNING: This example requires the cmd2 module: https://github.com/python-cmd2/cmd2 |
| 23 | +- pip install -U cmd2 |
| 24 | +""" |
| 25 | +import argparse |
| 26 | +from typing import Tuple |
| 27 | + |
| 28 | +import cmd2 |
| 29 | +import tableformatter as tf |
| 30 | + |
| 31 | +# Configure colors for when users chooses the "-c" flag to enable color in the table output |
| 32 | +try: |
| 33 | + from colored import bg |
| 34 | + BACK_PRI = bg(4) |
| 35 | + BACK_ALT = bg(22) |
| 36 | +except ImportError: |
| 37 | + try: |
| 38 | + from colorama import Back |
| 39 | + BACK_PRI = Back.LIGHTBLUE_EX |
| 40 | + BACK_ALT = Back.LIGHTYELLOW_EX |
| 41 | + except ImportError: |
| 42 | + BACK_PRI = '' |
| 43 | + BACK_ALT = '' |
| 44 | + |
| 45 | + |
| 46 | +# Formatter functions |
| 47 | +def no_dec(num: float) -> str: |
| 48 | + """Format a floating point number with no decimal places.""" |
| 49 | + return "{}".format(round(num)) |
| 50 | + |
| 51 | + |
| 52 | +def two_dec(num: float) -> str: |
| 53 | + """Format a floating point number with 2 decimal places.""" |
| 54 | + return "{0:.2f}".format(num) |
| 55 | + |
| 56 | + |
| 57 | +# Population data from Wikipedia: https://en.wikipedia.org/wiki/List_of_cities_proper_by_population |
| 58 | + |
| 59 | +# ############ Table data formatted as an iterable of iterable fields ############ |
| 60 | +EXAMPLE_ITERABLE_DATA = [['Shanghai (上海)', 'Shanghai', 'China', 'Asia', 24183300, 6340.5], |
| 61 | + ['Beijing (北京市)', 'Hebei', 'China', 'Asia', 20794000, 1749.57], |
| 62 | + ['Karachi (کراچی)', 'Sindh', 'Pakistan', 'Asia', 14910352, 615.58], |
| 63 | + ['Shenzen (深圳市)', 'Guangdong', 'China', 'Asia', 13723000, 1493.32], |
| 64 | + ['Guangzho (广州市)', 'Guangdong', 'China', 'Asia', 13081000, 1347.81], |
| 65 | + ['Mumbai (मुंबई)', 'Maharashtra', 'India', 'Asia', 12442373, 465.78], |
| 66 | + ['Istanbul (İstanbuld)', 'Istanbul', 'Turkey', 'Eurasia', 12661000, 620.29], |
| 67 | + ] |
| 68 | + |
| 69 | +# Calculate population density |
| 70 | +for row in EXAMPLE_ITERABLE_DATA: |
| 71 | + row.append(row[-2]/row[-1]) |
| 72 | + |
| 73 | + |
| 74 | +# Column headers plus optional formatting info for each column |
| 75 | +COLUMNS = [tf.Column('City', width=11, header_halign=tf.ColumnAlignment.AlignCenter), |
| 76 | + tf.Column('Province', header_halign=tf.ColumnAlignment.AlignCenter), |
| 77 | + 'Country', # NOTE: If you don't need any special effects, you can just pass a string |
| 78 | + tf.Column('Continent', cell_halign=tf.ColumnAlignment.AlignCenter), |
| 79 | + tf.Column('Population', cell_halign=tf.ColumnAlignment.AlignRight, formatter=tf.FormatCommas()), |
| 80 | + tf.Column('Area (km²)', width=7, header_halign=tf.ColumnAlignment.AlignCenter, |
| 81 | + cell_halign=tf.ColumnAlignment.AlignRight, formatter=two_dec), |
| 82 | + tf.Column('Pop. Density (/km²)', width=12, header_halign=tf.ColumnAlignment.AlignCenter, |
| 83 | + cell_halign=tf.ColumnAlignment.AlignRight, formatter=no_dec), |
| 84 | + ] |
| 85 | + |
| 86 | + |
| 87 | +# ######## Table data formatted as an iterable of python objects ######### |
| 88 | + |
| 89 | +class CityInfo(object): |
| 90 | + """City information container""" |
| 91 | + def __init__(self, city: str, province: str, country: str, continent: str, population: int, area: float): |
| 92 | + self.city = city |
| 93 | + self.province = province |
| 94 | + self.country = country |
| 95 | + self.continent = continent |
| 96 | + self._population = population |
| 97 | + self._area = area |
| 98 | + |
| 99 | + def get_population(self): |
| 100 | + """Population of the city""" |
| 101 | + return self._population |
| 102 | + |
| 103 | + def get_area(self): |
| 104 | + """Area of city in km²""" |
| 105 | + return self._area |
| 106 | + |
| 107 | + |
| 108 | +def pop_density(data: CityInfo) -> str: |
| 109 | + """Calculate the population density from the data entry""" |
| 110 | + if not isinstance(data, CityInfo): |
| 111 | + raise AttributeError("Argument to pop_density() must be an instance of CityInfo") |
| 112 | + return no_dec(data.get_population() / data.get_area()) |
| 113 | + |
| 114 | + |
| 115 | +# Convert the Iterable of Iterables data to an Iterable of non-iterable objects for demonstration purposes |
| 116 | +EXAMPLE_OBJECT_DATA = [] |
| 117 | +for city_data in EXAMPLE_ITERABLE_DATA: |
| 118 | + # Pass all city data other than population density to construct CityInfo |
| 119 | + EXAMPLE_OBJECT_DATA.append(CityInfo(*city_data[:-1])) |
| 120 | + |
| 121 | +# If table entries are python objects, all columns must be defined with the object attribute to query for each field |
| 122 | +# - attributes can be fields or functions. If a function is provided, the formatter will automatically call |
| 123 | +# the function to retrieve the value |
| 124 | +OBJ_COLS = [tf.Column('City', attrib='city', header_halign=tf.ColumnAlignment.AlignCenter), |
| 125 | + tf.Column('Province', attrib='province', header_halign=tf.ColumnAlignment.AlignCenter), |
| 126 | + tf.Column('Country', attrib='country'), |
| 127 | + tf.Column('Continent', attrib='continent', cell_halign=tf.ColumnAlignment.AlignCenter), |
| 128 | + tf.Column('Population', attrib='get_population', cell_halign=tf.ColumnAlignment.AlignRight, |
| 129 | + formatter=tf.FormatCommas()), |
| 130 | + tf.Column('Area (km²)', attrib='get_area', width=7, header_halign=tf.ColumnAlignment.AlignCenter, |
| 131 | + cell_halign=tf.ColumnAlignment.AlignRight, formatter=two_dec), |
| 132 | + tf.Column('Pop. Density (/km²)', width=12, header_halign=tf.ColumnAlignment.AlignCenter, |
| 133 | + cell_halign=tf.ColumnAlignment.AlignRight, obj_formatter=pop_density), |
| 134 | + ] |
| 135 | + |
| 136 | + |
| 137 | +EXTREMELY_HIGH_POULATION_DENSITY = 25000 |
| 138 | + |
| 139 | + |
| 140 | +def high_density_tuples(row_tuple: Tuple) -> dict: |
| 141 | + """Color rows with extremely high population density red.""" |
| 142 | + opts = dict() |
| 143 | + if len(row_tuple) >= 7 and row_tuple[6] > EXTREMELY_HIGH_POULATION_DENSITY: |
| 144 | + opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_RED |
| 145 | + return opts |
| 146 | + |
| 147 | + |
| 148 | +def high_density_objs(row_obj: CityInfo) -> dict: |
| 149 | + """Color rows with extremely high population density red.""" |
| 150 | + opts = dict() |
| 151 | + if float(pop_density(row_obj)) > EXTREMELY_HIGH_POULATION_DENSITY: |
| 152 | + opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_RED |
| 153 | + return opts |
| 154 | + |
| 155 | + |
| 156 | +class TableDisplay(cmd2.Cmd): |
| 157 | + """Example cmd2 application showing how you can display tabular data.""" |
| 158 | + |
| 159 | + def __init__(self): |
| 160 | + super().__init__() |
| 161 | + |
| 162 | + def ptable(self, rows, columns, grid_args, row_stylist): |
| 163 | + """Format tabular data for pretty-printing as a fixed-width table and then display it using a pager. |
| 164 | +
|
| 165 | + :param rows: required argument - can be a list-of-lists (or another iterable of iterables), a two-dimensional |
| 166 | + NumPy array, or an Iterable of non-iterable objects |
| 167 | + :param columns: column headers and formatting options per column |
| 168 | + :param grid_args: argparse arguments for formatting the grid |
| 169 | + :param row_stylist: function to determine how each row gets styled |
| 170 | + """ |
| 171 | + if grid_args.color: |
| 172 | + grid = tf.AlternatingRowGrid(BACK_PRI, BACK_ALT) |
| 173 | + elif grid_args.fancy: |
| 174 | + grid = tf.FancyGrid() |
| 175 | + elif grid_args.sparse: |
| 176 | + grid = tf.SparseGrid() |
| 177 | + else: |
| 178 | + grid = None |
| 179 | + |
| 180 | + formatted_table = tf.generate_table(rows=rows, columns=columns, grid_style=grid, row_tagger=row_stylist) |
| 181 | + self.ppaged(formatted_table, chop=True) |
| 182 | + |
| 183 | + table_parser = argparse.ArgumentParser() |
| 184 | + table_item_group = table_parser.add_mutually_exclusive_group() |
| 185 | + table_item_group.add_argument('-c', '--color', action='store_true', help='Enable color') |
| 186 | + table_item_group.add_argument('-f', '--fancy', action='store_true', help='Fancy Grid') |
| 187 | + table_item_group.add_argument('-s', '--sparse', action='store_true', help='Sparse Grid') |
| 188 | + |
| 189 | + @cmd2.with_argparser(table_parser) |
| 190 | + def do_table(self, args): |
| 191 | + """Display data in iterable form on the Earth's most populated cities in a table.""" |
| 192 | + self.ptable(EXAMPLE_ITERABLE_DATA, COLUMNS, args, high_density_tuples) |
| 193 | + |
| 194 | + @cmd2.with_argparser(table_parser) |
| 195 | + def do_object_table(self, args): |
| 196 | + """Display data in object form on the Earth's most populated cities in a table.""" |
| 197 | + self.ptable(EXAMPLE_OBJECT_DATA, OBJ_COLS, args, high_density_objs) |
| 198 | + |
| 199 | + |
| 200 | +if __name__ == '__main__': |
| 201 | + app = TableDisplay() |
| 202 | + app.debug = True |
| 203 | + app.cmdloop() |
0 commit comments