Merge "add formattable columns concept"
This commit is contained in:
commit
50af230c7c
28
cliff/columns.py
Normal file
28
cliff/columns.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Formattable column tools.
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FormattableColumn(object):
|
||||
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
@abc.abstractmethod
|
||||
def human_readable(self):
|
||||
"""Return a basic human readable version of the data.
|
||||
"""
|
||||
|
||||
def machine_readable(self):
|
||||
"""Return a raw data structure using only Python built-in types.
|
||||
|
||||
It must be possible to serialize the return value directly
|
||||
using a formatter like JSON, and it will be up to the
|
||||
formatter plugin to decide how to make that transformation.
|
||||
|
||||
"""
|
||||
return self._value
|
@ -26,11 +26,18 @@ class ListFormatter(Formatter):
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
"""Format and print the list from the iterable data source.
|
||||
|
||||
Data values can be primitive types like ints and strings, or
|
||||
can be an instance of a :class:`FormattableColumn` for
|
||||
situations where the value is complex, and may need to be
|
||||
handled differently for human readable output vs. machine
|
||||
readable output.
|
||||
|
||||
:param column_names: names of the columns
|
||||
:param data: iterable data source, one tuple per object
|
||||
with values in order of column names
|
||||
:param stdout: output stream where data should be written
|
||||
:param parsed_args: argparse namespace from our local options
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@ -43,6 +50,12 @@ class SingleFormatter(Formatter):
|
||||
def emit_one(self, column_names, data, stdout, parsed_args):
|
||||
"""Format and print the values associated with the single object.
|
||||
|
||||
Data values can be primitive types like ints and strings, or
|
||||
can be an instance of a :class:`FormattableColumn` for
|
||||
situations where the value is complex, and may need to be
|
||||
handled differently for human readable output vs. machine
|
||||
readable output.
|
||||
|
||||
:param column_names: names of the columns
|
||||
:param data: iterable data source with values in order of column names
|
||||
:param stdout: output stream where data should be written
|
||||
|
@ -5,6 +5,9 @@ import os
|
||||
import sys
|
||||
|
||||
from .base import ListFormatter
|
||||
from cliff import columns
|
||||
|
||||
import six
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
import csv
|
||||
@ -35,8 +38,14 @@ class CSVLister(ListFormatter):
|
||||
writer = csv.writer(stdout,
|
||||
quoting=self.QUOTE_MODES[parsed_args.quote_mode],
|
||||
lineterminator=os.linesep,
|
||||
escapechar='\\',
|
||||
)
|
||||
writer.writerow(column_names)
|
||||
for row in data:
|
||||
writer.writerow(row)
|
||||
writer.writerow(
|
||||
[(six.text_type(c.machine_readable())
|
||||
if isinstance(c, columns.FormattableColumn)
|
||||
else c)
|
||||
for c in row]
|
||||
)
|
||||
return
|
||||
|
@ -4,6 +4,7 @@
|
||||
import json
|
||||
|
||||
from .base import ListFormatter, SingleFormatter
|
||||
from cliff import columns
|
||||
|
||||
|
||||
class JSONFormatter(ListFormatter, SingleFormatter):
|
||||
@ -20,11 +21,21 @@ class JSONFormatter(ListFormatter, SingleFormatter):
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
items = []
|
||||
for item in data:
|
||||
items.append(dict(zip(column_names, item)))
|
||||
items.append(
|
||||
{n: (i.machine_readable()
|
||||
if isinstance(i, columns.FormattableColumn)
|
||||
else i)
|
||||
for n, i in zip(column_names, item)}
|
||||
)
|
||||
indent = None if parsed_args.noindent else 2
|
||||
json.dump(items, stdout, indent=indent)
|
||||
|
||||
def emit_one(self, column_names, data, stdout, parsed_args):
|
||||
one = dict(zip(column_names, data))
|
||||
one = {
|
||||
n: (i.machine_readable()
|
||||
if isinstance(i, columns.FormattableColumn)
|
||||
else i)
|
||||
for n, i in zip(column_names, data)
|
||||
}
|
||||
indent = None if parsed_args.noindent else 2
|
||||
json.dump(one, stdout, indent=indent)
|
||||
|
@ -2,6 +2,7 @@
|
||||
"""
|
||||
|
||||
from .base import SingleFormatter
|
||||
from cliff import columns
|
||||
|
||||
import argparse
|
||||
import six
|
||||
@ -37,6 +38,9 @@ class ShellFormatter(SingleFormatter):
|
||||
desired_columns = parsed_args.variables
|
||||
for name, value in zip(variable_names, data):
|
||||
if name in desired_columns or not desired_columns:
|
||||
value = (six.text_type(value.machine_readable())
|
||||
if isinstance(value, columns.FormattableColumn)
|
||||
else value)
|
||||
if isinstance(value, six.string_types):
|
||||
value = value.replace('"', '\\"')
|
||||
stdout.write('%s%s="%s"\n' % (parsed_args.prefix, name, value))
|
||||
|
@ -7,6 +7,18 @@ import os
|
||||
|
||||
from cliff import utils
|
||||
from .base import ListFormatter, SingleFormatter
|
||||
from cliff import columns
|
||||
|
||||
|
||||
def _format_row(row):
|
||||
new_row = []
|
||||
for r in row:
|
||||
if isinstance(r, columns.FormattableColumn):
|
||||
r = r.human_readable()
|
||||
if isinstance(r, six.string_types):
|
||||
r = r.replace('\r\n', '\n').replace('\r', ' ')
|
||||
new_row.append(r)
|
||||
return new_row
|
||||
|
||||
|
||||
class TableFormatter(ListFormatter, SingleFormatter):
|
||||
@ -52,12 +64,9 @@ class TableFormatter(ListFormatter, SingleFormatter):
|
||||
alignment = self.ALIGNMENTS.get(type(value), 'l')
|
||||
x.align[name] = alignment
|
||||
# Now iterate over the data and add the rows.
|
||||
x.add_row(first_row)
|
||||
x.add_row(_format_row(first_row))
|
||||
for row in data_iter:
|
||||
row = [r.replace('\r\n', '\n').replace('\r', ' ')
|
||||
if isinstance(r, six.string_types) else r
|
||||
for r in row]
|
||||
x.add_row(row)
|
||||
x.add_row(_format_row(row))
|
||||
|
||||
# Choose a reasonable min_width to better handle many columns on a
|
||||
# narrow console. The table will overflow the console width in
|
||||
@ -80,9 +89,7 @@ class TableFormatter(ListFormatter, SingleFormatter):
|
||||
x.align['Field'] = 'l'
|
||||
x.align['Value'] = 'l'
|
||||
for name, value in zip(column_names, data):
|
||||
value = (value.replace('\r\n', '\n').replace('\r', ' ') if
|
||||
isinstance(value, six.string_types) else value)
|
||||
x.add_row((name, value))
|
||||
x.add_row(_format_row((name, value)))
|
||||
|
||||
# Choose a reasonable min_width to better handle a narrow
|
||||
# console. The table will overflow the console width in preference
|
||||
|
@ -5,6 +5,7 @@ import six
|
||||
|
||||
from .base import ListFormatter
|
||||
from .base import SingleFormatter
|
||||
from cliff import columns
|
||||
|
||||
|
||||
class ValueFormatter(ListFormatter, SingleFormatter):
|
||||
@ -14,10 +15,19 @@ class ValueFormatter(ListFormatter, SingleFormatter):
|
||||
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
for row in data:
|
||||
stdout.write(' '.join(map(six.text_type, row)) + u'\n')
|
||||
stdout.write(
|
||||
' '.join(
|
||||
six.text_type(c.machine_readable()
|
||||
if isinstance(c, columns.FormattableColumn)
|
||||
else c)
|
||||
for c in row) + u'\n')
|
||||
return
|
||||
|
||||
def emit_one(self, column_names, data, stdout, parsed_args):
|
||||
for value in data:
|
||||
stdout.write('%s\n' % str(value))
|
||||
stdout.write('%s\n' % six.text_type(
|
||||
value.machine_readable()
|
||||
if isinstance(value, columns.FormattableColumn)
|
||||
else value)
|
||||
)
|
||||
return
|
||||
|
@ -4,6 +4,7 @@
|
||||
import yaml
|
||||
|
||||
from .base import ListFormatter, SingleFormatter
|
||||
from cliff import columns
|
||||
|
||||
|
||||
class YAMLFormatter(ListFormatter, SingleFormatter):
|
||||
@ -14,10 +15,19 @@ class YAMLFormatter(ListFormatter, SingleFormatter):
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
items = []
|
||||
for item in data:
|
||||
items.append(dict(zip(column_names, item)))
|
||||
items.append(
|
||||
{n: (i.machine_readable()
|
||||
if isinstance(i, columns.FormattableColumn)
|
||||
else i)
|
||||
for n, i in zip(column_names, item)}
|
||||
)
|
||||
yaml.safe_dump(items, stream=stdout, default_flow_style=False)
|
||||
|
||||
def emit_one(self, column_names, data, stdout, parsed_args):
|
||||
for key, value in zip(column_names, data):
|
||||
dict_data = {key: value}
|
||||
dict_data = {
|
||||
key: (value.machine_readable()
|
||||
if isinstance(value, columns.FormattableColumn)
|
||||
else value)
|
||||
}
|
||||
yaml.safe_dump(dict_data, stream=stdout, default_flow_style=False)
|
||||
|
18
cliff/tests/test_columns.py
Normal file
18
cliff/tests/test_columns.py
Normal file
@ -0,0 +1,18 @@
|
||||
from cliff import columns
|
||||
|
||||
|
||||
class FauxColumn(columns.FormattableColumn):
|
||||
|
||||
def human_readable(self):
|
||||
return u'I made this string myself: {}'.format(self._value)
|
||||
|
||||
|
||||
def test_faux_column_machine():
|
||||
c = FauxColumn(['list', 'of', 'values'])
|
||||
assert c.machine_readable() == ['list', 'of', 'values']
|
||||
|
||||
|
||||
def test_faux_column_human():
|
||||
c = FauxColumn(['list', 'of', 'values'])
|
||||
assert c.human_readable() == \
|
||||
u"I made this string myself: ['list', 'of', 'values']"
|
@ -6,6 +6,7 @@ import argparse
|
||||
import six
|
||||
|
||||
from cliff.formatters import commaseparated
|
||||
from cliff.tests import test_columns
|
||||
|
||||
|
||||
def test_commaseparated_list_formatter():
|
||||
@ -40,6 +41,20 @@ def test_commaseparated_list_formatter_quoted():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_commaseparated_list_formatter_formattable_column():
|
||||
sf = commaseparated.CSVLister()
|
||||
c = ('a', 'b', 'c')
|
||||
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
|
||||
data = [d1]
|
||||
expected = 'a,b,c\nA,B,[\'the\'\\, \'value\']\n'
|
||||
output = six.StringIO()
|
||||
parsed_args = mock.Mock()
|
||||
parsed_args.quote_mode = 'none'
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_commaseparated_list_formatter_unicode():
|
||||
sf = commaseparated.CSVLister()
|
||||
c = (u'a', u'b', u'c')
|
||||
|
@ -3,6 +3,7 @@ from six import StringIO
|
||||
import json
|
||||
|
||||
from cliff.formatters import json_format
|
||||
from cliff.tests import test_columns
|
||||
|
||||
import mock
|
||||
|
||||
@ -38,6 +39,29 @@ def test_json_format_one():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_json_format_formattablecolumn_one():
|
||||
sf = json_format.JSONFormatter()
|
||||
c = ('a', 'b', 'c', 'd')
|
||||
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
|
||||
expected = {
|
||||
'a': 'A',
|
||||
'b': 'B',
|
||||
'c': 'C',
|
||||
'd': ['the', 'value'],
|
||||
}
|
||||
args = mock.Mock()
|
||||
sf.add_argument_group(args)
|
||||
|
||||
args.noindent = True
|
||||
output = StringIO()
|
||||
sf.emit_one(c, d, output, args)
|
||||
value = output.getvalue()
|
||||
print(len(value.splitlines()))
|
||||
assert 1 == len(value.splitlines())
|
||||
actual = json.loads(value)
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_json_format_list():
|
||||
sf = json_format.JSONFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
@ -69,3 +93,24 @@ def test_json_format_list():
|
||||
assert 17 == len(value.splitlines())
|
||||
actual = json.loads(value)
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_json_format_formattablecolumn_list():
|
||||
sf = json_format.JSONFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
d = (
|
||||
('A1', 'B1', test_columns.FauxColumn(['the', 'value'])),
|
||||
)
|
||||
expected = [
|
||||
{'a': 'A1', 'b': 'B1', 'c': ['the', 'value']},
|
||||
]
|
||||
args = mock.Mock()
|
||||
sf.add_argument_group(args)
|
||||
|
||||
args.noindent = True
|
||||
output = StringIO()
|
||||
sf.emit_list(c, d, output, args)
|
||||
value = output.getvalue()
|
||||
assert 1 == len(value.splitlines())
|
||||
actual = json.loads(value)
|
||||
assert expected == actual
|
||||
|
@ -5,6 +5,7 @@ import argparse
|
||||
from six import StringIO, text_type
|
||||
|
||||
from cliff.formatters import shell
|
||||
from cliff.tests import test_columns
|
||||
|
||||
import mock
|
||||
|
||||
@ -38,6 +39,24 @@ def test_shell_formatter_args():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_shell_formatter_formattable_column():
|
||||
sf = shell.ShellFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
d = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
|
||||
expected = '\n'.join([
|
||||
'a="A"',
|
||||
'b="B"',
|
||||
'c="[\'the\', \'value\']"\n',
|
||||
])
|
||||
output = StringIO()
|
||||
args = mock.Mock()
|
||||
args.variables = ['a', 'b', 'c']
|
||||
args.prefix = ''
|
||||
sf.emit_one(c, d, output, args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_shell_formatter_with_non_string_values():
|
||||
sf = shell.ShellFormatter()
|
||||
c = ('a', 'b', 'c', 'd', 'e')
|
||||
|
@ -6,6 +6,7 @@ import os
|
||||
import argparse
|
||||
|
||||
from cliff.formatters import table
|
||||
from cliff.tests import test_columns
|
||||
|
||||
|
||||
class args(object):
|
||||
@ -65,7 +66,6 @@ def test_table_formatter(tw):
|
||||
'''
|
||||
assert expected == _table_tester_helper(c, d)
|
||||
|
||||
|
||||
# Multi-line output when width is restricted to 42 columns
|
||||
expected_ml_val = '''\
|
||||
+-------+--------------------------------+
|
||||
@ -237,6 +237,24 @@ def test_table_list_formatter(tw):
|
||||
assert expected == _table_tester_helper(c, data)
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_formatter_formattable_column(tw):
|
||||
tw.return_value = 0
|
||||
c = ('a', 'b', 'c', 'd')
|
||||
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
|
||||
expected = '''\
|
||||
+-------+---------------------------------------------+
|
||||
| Field | Value |
|
||||
+-------+---------------------------------------------+
|
||||
| a | A |
|
||||
| b | B |
|
||||
| c | C |
|
||||
| d | I made this string myself: ['the', 'value'] |
|
||||
+-------+---------------------------------------------+
|
||||
'''
|
||||
assert expected == _table_tester_helper(c, d)
|
||||
|
||||
|
||||
_col_names = ('one', 'two', 'three')
|
||||
_col_data = [(
|
||||
'one one one one one',
|
||||
@ -301,6 +319,22 @@ _expected_mv = {
|
||||
}
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_list_formatter_formattable_column(tw):
|
||||
tw.return_value = 80
|
||||
c = ('a', 'b', 'c')
|
||||
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
|
||||
data = [d1]
|
||||
expected = '''\
|
||||
+---+---+---------------------------------------------+
|
||||
| a | b | c |
|
||||
+---+---+---------------------------------------------+
|
||||
| A | B | I made this string myself: ['the', 'value'] |
|
||||
+---+---+---------------------------------------------+
|
||||
'''
|
||||
assert expected == _table_tester_helper(c, data)
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_list_formatter_max_width(tw):
|
||||
# no resize
|
||||
|
@ -2,6 +2,7 @@
|
||||
from six import StringIO
|
||||
|
||||
from cliff.formatters import value
|
||||
from cliff.tests import test_columns
|
||||
|
||||
|
||||
def test_value_formatter():
|
||||
@ -15,6 +16,17 @@ def test_value_formatter():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_value_formatter_formattable_column():
|
||||
sf = value.ValueFormatter()
|
||||
c = ('a', 'b', 'c', 'd')
|
||||
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
|
||||
expected = "A\nB\nC\n['the', 'value']\n"
|
||||
output = StringIO()
|
||||
sf.emit_one(c, d, output, None)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_value_list_formatter():
|
||||
sf = value.ValueFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
@ -26,3 +38,15 @@ def test_value_list_formatter():
|
||||
sf.emit_list(c, data, output, None)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_value_list_formatter_formattable_column():
|
||||
sf = value.ValueFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
|
||||
data = [d1]
|
||||
expected = "A B ['the', 'value']\n"
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, None)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
@ -3,6 +3,7 @@ from six import StringIO
|
||||
import yaml
|
||||
|
||||
from cliff.formatters import yaml_format
|
||||
from cliff.tests import test_columns
|
||||
|
||||
import mock
|
||||
|
||||
@ -24,6 +25,28 @@ def test_yaml_format_one():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_yaml_format_formattablecolumn_one():
|
||||
sf = yaml_format.YAMLFormatter()
|
||||
c = ('a', 'b', 'c', 'd')
|
||||
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
|
||||
expected = {
|
||||
'a': 'A',
|
||||
'b': 'B',
|
||||
'c': 'C',
|
||||
'd': ['the', 'value'],
|
||||
}
|
||||
args = mock.Mock()
|
||||
sf.add_argument_group(args)
|
||||
|
||||
args.noindent = True
|
||||
output = StringIO()
|
||||
sf.emit_one(c, d, output, args)
|
||||
value = output.getvalue()
|
||||
print(len(value.splitlines()))
|
||||
actual = yaml.safe_load(output.getvalue())
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_yaml_format_list():
|
||||
sf = yaml_format.YAMLFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
@ -43,3 +66,22 @@ def test_yaml_format_list():
|
||||
sf.emit_list(c, d, output, args)
|
||||
actual = yaml.safe_load(output.getvalue())
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_yaml_format_formattablecolumn_list():
|
||||
sf = yaml_format.YAMLFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
d = (
|
||||
('A1', 'B1', test_columns.FauxColumn(['the', 'value'])),
|
||||
)
|
||||
expected = [
|
||||
{'a': 'A1', 'b': 'B1', 'c': ['the', 'value']},
|
||||
]
|
||||
args = mock.Mock()
|
||||
sf.add_argument_group(args)
|
||||
|
||||
args.noindent = True
|
||||
output = StringIO()
|
||||
sf.emit_list(c, d, output, args)
|
||||
actual = yaml.safe_load(output.getvalue())
|
||||
assert expected == actual
|
||||
|
Loading…
Reference in New Issue
Block a user