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