Resize columns to fit screen width
Many outputs from python-openstackclient will exceed the width of the
user's terminal. When this happens even by one character, the whole
PrettyTable layout becomes very difficult to read.
This change adds a utils.terminal_width method and uses that to
calculate optimal max_width values to fit the table into the
available width.
Notes on the utils.terminal_width implementation:
- Determining width by consuming the COLUMNS environment variable is
avoided so that existing scripts which pipe output for value parsing
continue to behave deterministically.
- python3 has support for getting the terminal size, so this is used
when available
- The python2 fallback uses the same linux tty detection found in many
python recipes
- None is returned when width cannot be determined, such as
- Not a tty, because the command is being piped to other commands, or
it is being run in a non-interactive environment
- A python2 environment running on Windows or OSX (these can always be
added later by engineers who have access to these environments)
The previous column width behaviour is retained in the following
scenarios:
- utils.terminal_width returns None for any of the reasons stated
above
- --max-width is specified
Closes-Bug: #1485847
Change-Id: I51b6157929f0b4a9ce66990e3e64ce9e730862c2
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import mock
|
||||
from six import StringIO
|
||||
|
||||
from cliff.formatters import table
|
||||
@@ -9,7 +11,9 @@ class args(object):
|
||||
self.max_width = max_width
|
||||
|
||||
|
||||
def test_table_formatter():
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_formatter(tw):
|
||||
tw.return_value = 80
|
||||
sf = table.TableFormatter()
|
||||
c = ('a', 'b', 'c', 'd')
|
||||
d = ('A', 'B', 'C', 'test\rcarriage\r\nreturn')
|
||||
@@ -31,7 +35,83 @@ def test_table_formatter():
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_table_list_formatter():
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_formatter_max_width(tw):
|
||||
tw.return_value = 80
|
||||
sf = table.TableFormatter()
|
||||
c = ('field_name', 'a_really_long_field_name')
|
||||
d = ('the value', 'a value significantly longer than the field')
|
||||
expected = '''\
|
||||
+--------------------------+---------------------------------------------+
|
||||
| Field | Value |
|
||||
+--------------------------+---------------------------------------------+
|
||||
| field_name | the value |
|
||||
| a_really_long_field_name | a value significantly longer than the field |
|
||||
+--------------------------+---------------------------------------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
parsed_args = args()
|
||||
sf.emit_one(c, d, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
# resize value column
|
||||
tw.return_value = 70
|
||||
expected = '''\
|
||||
+--------------------------+-----------------------------------------+
|
||||
| Field | Value |
|
||||
+--------------------------+-----------------------------------------+
|
||||
| field_name | the value |
|
||||
| a_really_long_field_name | a value significantly longer than the |
|
||||
| | field |
|
||||
+--------------------------+-----------------------------------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
parsed_args = args()
|
||||
sf.emit_one(c, d, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
# resize both columns
|
||||
tw.return_value = 50
|
||||
expected = '''\
|
||||
+-----------------------+------------------------+
|
||||
| Field | Value |
|
||||
+-----------------------+------------------------+
|
||||
| field_name | the value |
|
||||
| a_really_long_field_n | a value significantly |
|
||||
| ame | longer than the field |
|
||||
+-----------------------+------------------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
parsed_args = args()
|
||||
sf.emit_one(c, d, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
# resize all columns limited by min_width=16
|
||||
tw.return_value = 10
|
||||
expected = '''\
|
||||
+------------------+------------------+
|
||||
| Field | Value |
|
||||
+------------------+------------------+
|
||||
| field_name | the value |
|
||||
| a_really_long_fi | a value |
|
||||
| eld_name | significantly |
|
||||
| | longer than the |
|
||||
| | field |
|
||||
+------------------+------------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
parsed_args = args()
|
||||
sf.emit_one(c, d, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_list_formatter(tw):
|
||||
tw.return_value = 80
|
||||
sf = table.TableFormatter()
|
||||
c = ('a', 'b', 'c')
|
||||
d1 = ('A', 'B', 'C')
|
||||
@@ -51,3 +131,128 @@ def test_table_list_formatter():
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.terminal_width')
|
||||
def test_table_list_formatter_max_width(tw):
|
||||
tw.return_value = 80
|
||||
sf = table.TableFormatter()
|
||||
c = ('one', 'two', 'three')
|
||||
d1 = (
|
||||
'one one one one one',
|
||||
'two two two two',
|
||||
'three three')
|
||||
data = [d1]
|
||||
parsed_args = args()
|
||||
|
||||
# no resize
|
||||
expected = '''\
|
||||
+---------------------+-----------------+-------------+
|
||||
| one | two | three |
|
||||
+---------------------+-----------------+-------------+
|
||||
| one one one one one | two two two two | three three |
|
||||
+---------------------+-----------------+-------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
|
||||
# resize 1 column
|
||||
tw.return_value = 50
|
||||
expected = '''\
|
||||
+----------------+-----------------+-------------+
|
||||
| one | two | three |
|
||||
+----------------+-----------------+-------------+
|
||||
| one one one | two two two two | three three |
|
||||
| one one | | |
|
||||
+----------------+-----------------+-------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
assert len(actual.splitlines()[0]) == 50
|
||||
|
||||
# resize 2 columns
|
||||
tw.return_value = 45
|
||||
expected = '''\
|
||||
+--------------+--------------+-------------+
|
||||
| one | two | three |
|
||||
+--------------+--------------+-------------+
|
||||
| one one one | two two two | three three |
|
||||
| one one | two | |
|
||||
+--------------+--------------+-------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
assert len(actual.splitlines()[0]) == 45
|
||||
|
||||
# resize all columns
|
||||
tw.return_value = 40
|
||||
expected = '''\
|
||||
+------------+------------+------------+
|
||||
| one | two | three |
|
||||
+------------+------------+------------+
|
||||
| one one | two two | three |
|
||||
| one one | two two | three |
|
||||
| one | | |
|
||||
+------------+------------+------------+
|
||||
'''
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
assert len(actual.splitlines()[0]) == 40
|
||||
|
||||
# resize all columns limited by min_width=8
|
||||
tw.return_value = 10
|
||||
expected = '''\
|
||||
+----------+----------+----------+
|
||||
| one | two | three |
|
||||
+----------+----------+----------+
|
||||
| one one | two two | three |
|
||||
| one one | two two | three |
|
||||
| one | | |
|
||||
+----------+----------+----------+
|
||||
'''
|
||||
output = StringIO()
|
||||
sf.emit_list(c, data, output, parsed_args)
|
||||
actual = output.getvalue()
|
||||
assert expected == actual
|
||||
# 3 columns each 8 wide, plus table spacing and borders
|
||||
expected_width = 11 * 3 + 1
|
||||
assert len(actual.splitlines()[0]) == expected_width
|
||||
|
||||
|
||||
def test_field_widths():
|
||||
tf = table.TableFormatter
|
||||
assert {
|
||||
'a': 1,
|
||||
'b': 2,
|
||||
'c': 3,
|
||||
'd': 10
|
||||
} == tf._field_widths(
|
||||
('a', 'b', 'c', 'd'),
|
||||
'+---+----+-----+------------+')
|
||||
|
||||
|
||||
def test_field_widths_zero():
|
||||
tf = table.TableFormatter
|
||||
assert {
|
||||
'a': 0,
|
||||
'b': 0,
|
||||
'c': 0
|
||||
} == tf._field_widths(
|
||||
('a', 'b', 'c'),
|
||||
'+--+-++')
|
||||
|
||||
|
||||
def test_width_info():
|
||||
tf = table.TableFormatter
|
||||
assert (49, 4) == (tf._width_info(80, 10))
|
||||
assert (76, 76) == (tf._width_info(80, 1))
|
||||
assert (79, 0) == (tf._width_info(80, 0))
|
||||
assert (0, 0) == (tf._width_info(0, 80))
|
||||
|
||||
44
cliff/tests/test_utils.py
Normal file
44
cliff/tests/test_utils.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import mock
|
||||
import nose
|
||||
|
||||
from cliff import utils
|
||||
|
||||
|
||||
def test_utils_terminal_width():
|
||||
width = utils.terminal_width(sys.stdout)
|
||||
# Results are specific to the execution environment, so only assert
|
||||
# that no error is raised.
|
||||
assert width is None or isinstance(width, int)
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.os')
|
||||
def test_utils_terminal_width_get_terminal_size(mock_os):
|
||||
if not hasattr(os, 'get_terminal_size'):
|
||||
raise nose.SkipTest('only needed for python 3.3 onwards')
|
||||
ts = os.terminal_size((10, 5))
|
||||
mock_os.get_terminal_size.return_value = ts
|
||||
width = utils.terminal_width(sys.stdout)
|
||||
assert width == 10
|
||||
|
||||
mock_os.get_terminal_size.side_effect = OSError()
|
||||
width = utils.terminal_width(sys.stdout)
|
||||
assert width is None
|
||||
|
||||
|
||||
@mock.patch('cliff.utils.ioctl')
|
||||
def test_utils_terminal_width_ioctl(mock_ioctl):
|
||||
if hasattr(os, 'get_terminal_size'):
|
||||
raise nose.SkipTest('only needed for python 3.2 and before')
|
||||
mock_ioctl.return_value = struct.pack('hhhh', 57, 101, 0, 0)
|
||||
width = utils.terminal_width(sys.stdout)
|
||||
assert width == 101
|
||||
|
||||
mock_ioctl.side_effect = IOError()
|
||||
width = utils.terminal_width(sys.stdout)
|
||||
assert width is None
|
||||
Reference in New Issue
Block a user