summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-07-21 16:42:55 +0000
committerGerrit Code Review <review@openstack.org>2016-07-21 16:42:55 +0000
commit50af230c7cbe1ba012252773e875d7b65a79e86d (patch)
treeaedf80cbb382ba67b5b75ddd9db0290a6e43ad60
parent0ffb03603757d17777f3af1370725185a4cf4064 (diff)
parent94b1666bce150de5678fe7a9e42ef0604f7b7d75 (diff)
downloadcliff-50af230c7cbe1ba012252773e875d7b65a79e86d.tar.gz
Merge "add formattable columns concept"
-rw-r--r--cliff/columns.py28
-rw-r--r--cliff/formatters/base.py13
-rw-r--r--cliff/formatters/commaseparated.py11
-rw-r--r--cliff/formatters/json_format.py15
-rw-r--r--cliff/formatters/shell.py4
-rw-r--r--cliff/formatters/table.py23
-rw-r--r--cliff/formatters/value.py14
-rw-r--r--cliff/formatters/yaml_format.py14
-rw-r--r--cliff/tests/test_columns.py18
-rw-r--r--cliff/tests/test_formatters_csv.py15
-rw-r--r--cliff/tests/test_formatters_json.py45
-rw-r--r--cliff/tests/test_formatters_shell.py19
-rw-r--r--cliff/tests/test_formatters_table.py36
-rw-r--r--cliff/tests/test_formatters_value.py24
-rw-r--r--cliff/tests/test_formatters_yaml.py42
15 files changed, 305 insertions, 16 deletions
diff --git a/cliff/columns.py b/cliff/columns.py
new file mode 100644
index 0000000..3b6c026
--- /dev/null
+++ b/cliff/columns.py
@@ -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
diff --git a/cliff/formatters/base.py b/cliff/formatters/base.py
index c477a54..79e8920 100644
--- a/cliff/formatters/base.py
+++ b/cliff/formatters/base.py
@@ -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
diff --git a/cliff/formatters/commaseparated.py b/cliff/formatters/commaseparated.py
index c545f28..ce66d2a 100644
--- a/cliff/formatters/commaseparated.py
+++ b/cliff/formatters/commaseparated.py
@@ -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
diff --git a/cliff/formatters/json_format.py b/cliff/formatters/json_format.py
index fc35da1..6782499 100644
--- a/cliff/formatters/json_format.py
+++ b/cliff/formatters/json_format.py
@@ -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)
diff --git a/cliff/formatters/shell.py b/cliff/formatters/shell.py
index e613c22..5525e22 100644
--- a/cliff/formatters/shell.py
+++ b/cliff/formatters/shell.py
@@ -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))
diff --git a/cliff/formatters/table.py b/cliff/formatters/table.py
index 8e51859..b62d7e3 100644
--- a/cliff/formatters/table.py
+++ b/cliff/formatters/table.py
@@ -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
diff --git a/cliff/formatters/value.py b/cliff/formatters/value.py
index 6cc6744..d28f928 100644
--- a/cliff/formatters/value.py
+++ b/cliff/formatters/value.py
@@ -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
diff --git a/cliff/formatters/yaml_format.py b/cliff/formatters/yaml_format.py
index e6fe17d..083cf52 100644
--- a/cliff/formatters/yaml_format.py
+++ b/cliff/formatters/yaml_format.py
@@ -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)
diff --git a/cliff/tests/test_columns.py b/cliff/tests/test_columns.py
new file mode 100644
index 0000000..cbb8217
--- /dev/null
+++ b/cliff/tests/test_columns.py
@@ -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']"
diff --git a/cliff/tests/test_formatters_csv.py b/cliff/tests/test_formatters_csv.py
index cd2e4cf..510c790 100644
--- a/cliff/tests/test_formatters_csv.py
+++ b/cliff/tests/test_formatters_csv.py
@@ -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')
diff --git a/cliff/tests/test_formatters_json.py b/cliff/tests/test_formatters_json.py
index 26d6f5e..0ce902b 100644
--- a/cliff/tests/test_formatters_json.py
+++ b/cliff/tests/test_formatters_json.py
@@ -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
diff --git a/cliff/tests/test_formatters_shell.py b/cliff/tests/test_formatters_shell.py
index 956e16b..2babfd4 100644
--- a/cliff/tests/test_formatters_shell.py
+++ b/cliff/tests/test_formatters_shell.py
@@ -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')
diff --git a/cliff/tests/test_formatters_table.py b/cliff/tests/test_formatters_table.py
index a9cd975..0e093eb 100644
--- a/cliff/tests/test_formatters_table.py
+++ b/cliff/tests/test_formatters_table.py
@@ -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',
@@ -302,6 +320,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
l = tw.return_value = 80
diff --git a/cliff/tests/test_formatters_value.py b/cliff/tests/test_formatters_value.py
index a19a4d2..6ba9e9d 100644
--- a/cliff/tests/test_formatters_value.py
+++ b/cliff/tests/test_formatters_value.py
@@ -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
diff --git a/cliff/tests/test_formatters_yaml.py b/cliff/tests/test_formatters_yaml.py
index ef8805f..d64d1b7 100644
--- a/cliff/tests/test_formatters_yaml.py
+++ b/cliff/tests/test_formatters_yaml.py
@@ -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