Skip to content

table_display.py example now uses tableformatter instead of tabulate #456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 0.9.2 (TBD, 2018)
## 0.9.2 (June 28, 2018)
* Bug Fixes
* Fixed issue where piping and redirecting did not work correctly with paths that had spaces
* Enhancements
Expand All @@ -9,6 +9,8 @@
* Added ``chop`` argument to ``cmd2.Cmd.ppaged()`` method for displaying output using a pager
* If ``chop`` is ``False``, then ``self.pager`` is used as the pager
* Otherwise ``self.pager_chop`` is used as the pager
* Greatly improved the [table_display.py](https://github.com/python-cmd2/cmd2/blob/master/examples/table_display.py) example
* Now uses the new [tableformatter](https://github.com/python-tableformatter/tableformatter) module which looks better than ``tabulate``
* Deprecations
* The ``CmdResult`` helper class is *deprecated* and replaced by the improved ``CommandResult`` class
* ``CommandResult`` has the following attributes: **stdout**, **stderr**, and **data**
Expand Down
2 changes: 1 addition & 1 deletion cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def __subclasshook__(cls, C):
except ImportError: # pragma: no cover
ipython_available = False

__version__ = '0.9.2a'
__version__ = '0.9.2'


# optional attribute, when tagged on a function, allows cmd2 to categorize commands
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
# The short X.Y version.
version = '0.9'
# The full version, including alpha/beta/rc tags.
release = '0.9.2a'
release = '0.9.2'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
190 changes: 159 additions & 31 deletions examples/table_display.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,145 @@
#!/usr/bin/env python
# coding=utf-8
"""A simple example demonstrating the following:
1) How to display tabular data within a cmd2 application
1) How to display tabular data
2) How to display output using a pager

NOTE: IF the table does not entirely fit within the screen of your terminal, then it will be displayed using a pager.
You can use the arrow keys (left, right, up, and down) to scroll around the table as well as the PageUp/PageDown keys.
You can quit out of the pager by typing "q". You can also search for text within the pager using "/".

WARNING: This example requires the tabulate module.
WARNING: This example requires the tableformatter module: https://github.com/python-tableformatter/tableformatter
- pip install tableformatter
"""
import functools
import argparse
from typing import Tuple

import cmd2
import tabulate
import tableformatter as tf

# Format to use with tabulate module when displaying tables
TABLE_FORMAT = 'grid'
# Configure colors for when users chooses the "-c" flag to enable color in the table output
try:
from colored import bg
BACK_PRI = bg(4)
BACK_ALT = bg(22)
except ImportError:
try:
from colorama import Back
BACK_PRI = Back.LIGHTBLUE_EX
BACK_ALT = Back.LIGHTYELLOW_EX
except ImportError:
BACK_PRI = ''
BACK_ALT = ''


# Formatter functions
def no_dec(num: float) -> str:
"""Format a floating point number with no decimal places."""
return "{}".format(round(num))


def two_dec(num: float) -> str:
"""Format a floating point number with 2 decimal places."""
return "{0:.2f}".format(num)

# Create a function to format a fixed-width table for pretty-printing using the desired table format
table = functools.partial(tabulate.tabulate, tablefmt=TABLE_FORMAT)

# Population data from Wikipedia: https://en.wikipedia.org/wiki/List_of_cities_proper_by_population
EXAMPLE_DATA = [['Shanghai', 'Shanghai', 'China', 'Asia', 24183300, 6340.5, 3814],
['Beijing', 'Hebei', 'China', 'Asia', 20794000, 1749.57, 11885],
['Karachi', 'Sindh', 'Pakistan', 'Asia', 14910352, 615.58, 224221],
['Shenzen', 'Guangdong', 'China', 'Asia', 13723000, 1493.32, 9190],
['Guangzho', 'Guangdong', 'China', 'Asia', 13081000, 1347.81, 9705],
['Mumbai', ' Maharashtra', 'India', 'Asia', 12442373, 465.78, 27223],
['Istanbul', 'Istanbul', 'Turkey', 'Eurasia', 12661000, 620.29, 20411],
]
EXAMPLE_HEADERS = ['City', 'Province', 'Country', 'Continent', 'Population', 'Area (km^2)', 'Pop. Density (/km^2)']

# ############ Table data formatted as an iterable of iterable fields ############
EXAMPLE_ITERABLE_DATA = [['Shanghai (上海)', 'Shanghai', 'China', 'Asia', 24183300, 6340.5],
['Beijing (北京市)', 'Hebei', 'China', 'Asia', 20794000, 1749.57],
['Karachi (کراچی)', 'Sindh', 'Pakistan', 'Asia', 14910352, 615.58],
['Shenzen (深圳市)', 'Guangdong', 'China', 'Asia', 13723000, 1493.32],
['Guangzho (广州市)', 'Guangdong', 'China', 'Asia', 13081000, 1347.81],
['Mumbai (मुंबई)', 'Maharashtra', 'India', 'Asia', 12442373, 465.78],
['Istanbul (İstanbuld)', 'Istanbul', 'Turkey', 'Eurasia', 12661000, 620.29],
]

# Calculate population density
for row in EXAMPLE_ITERABLE_DATA:
row.append(row[-2]/row[-1])


# Column headers plus optional formatting info for each column
COLUMNS = [tf.Column('City', width=11, header_halign=tf.ColumnAlignment.AlignCenter),
tf.Column('Province', header_halign=tf.ColumnAlignment.AlignCenter),
'Country', # NOTE: If you don't need any special effects, you can just pass a string
tf.Column('Continent', cell_halign=tf.ColumnAlignment.AlignCenter),
tf.Column('Population', cell_halign=tf.ColumnAlignment.AlignRight, formatter=tf.FormatCommas()),
tf.Column('Area (km²)', width=7, header_halign=tf.ColumnAlignment.AlignCenter,
cell_halign=tf.ColumnAlignment.AlignRight, formatter=two_dec),
tf.Column('Pop. Density (/km²)', width=12, header_halign=tf.ColumnAlignment.AlignCenter,
cell_halign=tf.ColumnAlignment.AlignRight, formatter=no_dec),
]


# ######## Table data formatted as an iterable of python objects #########

class CityInfo(object):
"""City information container"""
def __init__(self, city: str, province: str, country: str, continent: str, population: int, area: float):
self.city = city
self.province = province
self.country = country
self.continent = continent
self._population = population
self._area = area

def get_population(self):
"""Population of the city"""
return self._population

def get_area(self):
"""Area of city in km²"""
return self._area


def pop_density(data: CityInfo) -> str:
"""Calculate the population density from the data entry"""
if not isinstance(data, CityInfo):
raise AttributeError("Argument to pop_density() must be an instance of CityInfo")
return no_dec(data.get_population() / data.get_area())


# Convert the Iterable of Iterables data to an Iterable of non-iterable objects for demonstration purposes
EXAMPLE_OBJECT_DATA = []
for city_data in EXAMPLE_ITERABLE_DATA:
# Pass all city data other than population density to construct CityInfo
EXAMPLE_OBJECT_DATA.append(CityInfo(*city_data[:-1]))

# If table entries are python objects, all columns must be defined with the object attribute to query for each field
# - attributes can be fields or functions. If a function is provided, the formatter will automatically call
# the function to retrieve the value
OBJ_COLS = [tf.Column('City', attrib='city', header_halign=tf.ColumnAlignment.AlignCenter),
tf.Column('Province', attrib='province', header_halign=tf.ColumnAlignment.AlignCenter),
tf.Column('Country', attrib='country'),
tf.Column('Continent', attrib='continent', cell_halign=tf.ColumnAlignment.AlignCenter),
tf.Column('Population', attrib='get_population', cell_halign=tf.ColumnAlignment.AlignRight,
formatter=tf.FormatCommas()),
tf.Column('Area (km²)', attrib='get_area', width=7, header_halign=tf.ColumnAlignment.AlignCenter,
cell_halign=tf.ColumnAlignment.AlignRight, formatter=two_dec),
tf.Column('Pop. Density (/km²)', width=12, header_halign=tf.ColumnAlignment.AlignCenter,
cell_halign=tf.ColumnAlignment.AlignRight, obj_formatter=pop_density),
]


EXTREMELY_HIGH_POULATION_DENSITY = 25000


def high_density_tuples(row_tuple: Tuple) -> dict:
"""Color rows with extremely high population density red."""
opts = dict()
if len(row_tuple) >= 7 and row_tuple[6] > EXTREMELY_HIGH_POULATION_DENSITY:
opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_RED
return opts


def high_density_objs(row_obj: CityInfo) -> dict:
"""Color rows with extremely high population density red."""
opts = dict()
if float(pop_density(row_obj)) > EXTREMELY_HIGH_POULATION_DENSITY:
opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_RED
return opts


class TableDisplay(cmd2.Cmd):
Expand All @@ -39,26 +148,45 @@ class TableDisplay(cmd2.Cmd):
def __init__(self):
super().__init__()

def ptable(self, tabular_data, headers=()):
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 tabular_data: required argument - can be a list-of-lists (or another iterable of iterables), a list of
named tuples, a dictionary of iterables, an iterable of dictionaries, a two-dimensional
NumPy array, NumPy record array, or a Pandas dataframe.
:param headers: (optional) - to print nice column headers, supply this argument:
- headers can be an explicit list of column headers
- if `headers="firstrow"`, then the first row of data is used
- if `headers="keys"`, then dictionary keys or column indices are used
- Otherwise, a headerless table is produced
: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
"""
formatted_table = table(tabular_data, headers=headers)
self.ppaged(formatted_table)
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

formatted_table = tf.generate_table(rows=rows, columns=columns, grid_style=grid, row_tagger=row_stylist)
self.ppaged(formatted_table, chop=True)

table_parser = argparse.ArgumentParser()
table_item_group = table_parser.add_mutually_exclusive_group()
table_item_group.add_argument('-c', '--color', action='store_true', help='Enable color')
table_item_group.add_argument('-f', '--fancy', action='store_true', help='Fancy Grid')
table_item_group.add_argument('-s', '--sparse', action='store_true', help='Sparse Grid')

@cmd2.with_argparser(table_parser)
def do_table(self, args):
"""Display data in iterable form on the Earth's most populated cities in a table."""
self.ptable(EXAMPLE_ITERABLE_DATA, COLUMNS, args, high_density_tuples)

def do_table(self, _):
"""Display data on the Earth's most populated cities in a table."""
self.ptable(tabular_data=EXAMPLE_DATA, headers=EXAMPLE_HEADERS)
@cmd2.with_argparser(table_parser)
def do_object_table(self, args):
"""Display data in object form on the Earth's most populated cities in a table."""
self.ptable(EXAMPLE_OBJECT_DATA, OBJ_COLS, args, high_density_objs)


if __name__ == '__main__':
app = TableDisplay()
app.debug = True
app.cmdloop()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""
from setuptools import setup

VERSION = '0.9.2a'
VERSION = '0.9.2'
DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python"
LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make
it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@


def test_ver():
assert cmd2.__version__ == '0.9.2a'
assert cmd2.__version__ == '0.9.2'


def test_empty_statement(base_app):
Expand Down