1
1
#!/usr/bin/env python
2
2
# coding=utf-8
3
3
"""A simple example demonstrating the following:
4
- 1) How to display tabular data within a cmd2 application
4
+ 1) How to display tabular data
5
5
2) How to display output using a pager
6
6
7
7
NOTE: IF the table does not entirely fit within the screen of your terminal, then it will be displayed using a pager.
8
8
You can use the arrow keys (left, right, up, and down) to scroll around the table as well as the PageUp/PageDown keys.
9
9
You can quit out of the pager by typing "q". You can also search for text within the pager using "/".
10
10
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
12
13
"""
13
- import functools
14
+ import argparse
15
+ from typing import Tuple
14
16
15
17
import cmd2
16
- import tabulate
18
+ import tableformatter as tf
17
19
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 )
20
44
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 )
23
45
24
46
# 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
34
143
35
144
36
145
class TableDisplay (cmd2 .Cmd ):
@@ -39,26 +148,45 @@ class TableDisplay(cmd2.Cmd):
39
148
def __init__ (self ):
40
149
super ().__init__ ()
41
150
42
- def ptable (self , tabular_data , headers = () ):
151
+ def ptable (self , rows , columns , grid_args , row_stylist ):
43
152
"""Format tabular data for pretty-printing as a fixed-width table and then display it using a pager.
44
153
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
53
159
"""
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 )
56
182
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 )
60
187
61
188
62
189
if __name__ == '__main__' :
63
190
app = TableDisplay ()
191
+ app .debug = True
64
192
app .cmdloop ()
0 commit comments