summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2020-01-20 21:50:31 -0500
committerGitHub <noreply@github.com>2020-01-20 21:50:31 -0500
commite4bc87da34bb6fc645f54a4467dc7d0e32dfaa3b (patch)
treed3a98dca7645c7acd94a418d72e05b46b5ba8bc7
parent2ffdefb6d1f7b2aab63aef529ea4bb46ead42b59 (diff)
parent2f52a849d5100f309745c5b3b1c91fc97de178a9 (diff)
downloadcmd2-git-e4bc87da34bb6fc645f54a4467dc7d0e32dfaa3b.tar.gz
Merge pull request #850 from python-cmd2/truncate_string
Truncate line
-rw-r--r--CHANGELOG.md3
-rw-r--r--cmd2/utils.py85
-rw-r--r--docs/api/utility_functions.rst2
-rw-r--r--docs/features/generating_output.rst18
-rw-r--r--docs/features/settings.rst17
-rw-r--r--tests/test_utils.py79
6 files changed, 164 insertions, 40 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5ee988f..73aa5b23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
* Enhancements
* Flushing stderr when setting the window title and printing alerts for better responsiveness in cases where
stderr is not unbuffered.
+ * Added function to truncate a single line to fit within a given display width. `cmd2.utils.truncate_line`
+ supports characters with display widths greater than 1 and ANSI style sequences.
+ * Added line truncation support to `cmd2.utils` text alignment functions.
## 0.9.23 (January 9, 2020)
* Bug Fixes
diff --git a/cmd2/utils.py b/cmd2/utils.py
index ffbe5a64..3e154bd2 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -638,7 +638,7 @@ class TextAlignment(Enum):
def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
- width: Optional[int] = None, tab_width: int = 4) -> str:
+ width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str:
"""
Align text for display within a given width. Supports characters with display widths greater than 1.
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -652,15 +652,24 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
be converted to a space.
+ :param truncate: if True, then each line will be shortened to fit within the display width. The truncated
+ portions are replaced by a '…' character. Defaults to False.
:return: aligned text
:raises: TypeError if fill_char is more than one character
ValueError if text or fill_char contains an unprintable character
+ ValueError if width is less than 1
"""
import io
import shutil
from . import ansi
+ if width is None:
+ width = shutil.get_terminal_size().columns
+
+ if width < 1:
+ raise ValueError("width must be at least 1")
+
# Handle tabs
text = text.replace('\t', ' ' * tab_width)
if fill_char == '\t':
@@ -678,23 +687,21 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
else:
lines = ['']
- if width is None:
- width = shutil.get_terminal_size().columns
-
text_buf = io.StringIO()
for index, line in enumerate(lines):
if index > 0:
text_buf.write('\n')
- # Use style_aware_wcswidth to support characters with display widths
- # greater than 1 as well as ANSI style sequences
+ if truncate:
+ line = truncate_line(line, width)
+
line_width = ansi.style_aware_wcswidth(line)
if line_width == -1:
raise(ValueError("Text to align contains an unprintable character"))
- # Check if line is wider than the desired final width
- if width <= line_width:
+ elif line_width >= width:
+ # No need to add fill characters
text_buf.write(line)
continue
@@ -725,7 +732,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
return text_buf.getvalue()
-def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
+def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
+ tab_width: int = 4, truncate: bool = False) -> str:
"""
Left align text for display within a given width. Supports characters with display widths greater than 1.
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -736,14 +744,19 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
be converted to a space.
+ :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
+ replaced by a '…' character. Defaults to False.
:return: left-aligned text
:raises: TypeError if fill_char is more than one character
ValueError if text or fill_char contains an unprintable character
+ ValueError if width is less than 1
"""
- return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width)
+ return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width,
+ tab_width=tab_width, truncate=truncate)
-def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
+def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
+ tab_width: int = 4, truncate: bool = False) -> str:
"""
Center text for display within a given width. Supports characters with display widths greater than 1.
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -754,14 +767,19 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
be converted to a space.
+ :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
+ replaced by a '…' character. Defaults to False.
:return: centered text
:raises: TypeError if fill_char is more than one character
ValueError if text or fill_char contains an unprintable character
+ ValueError if width is less than 1
"""
- return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width)
+ return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width,
+ tab_width=tab_width, truncate=truncate)
-def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
+def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
+ tab_width: int = 4, truncate: bool = False) -> str:
"""
Right align text for display within a given width. Supports characters with display widths greater than 1.
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -772,8 +790,47 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
be converted to a space.
+ :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
+ replaced by a '…' character. Defaults to False.
:return: right-aligned text
:raises: TypeError if fill_char is more than one character
ValueError if text or fill_char contains an unprintable character
+ ValueError if width is less than 1
+ """
+ return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width,
+ tab_width=tab_width, truncate=truncate)
+
+
+def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
"""
- return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width)
+ Truncate a single line to fit within a given display width. Any portion of the string that is truncated
+ is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences are
+ safely ignored and do not count toward the display width. This means colored text is supported.
+
+ :param line: text to truncate
+ :param max_width: the maximum display width the resulting string is allowed to have
+ :param tab_width: any tabs in the text will be replaced with this many spaces
+ :return: line that has a display width less than or equal to width
+ :raises: ValueError if text contains an unprintable character like a new line
+ ValueError if max_width is less than 1
+ """
+ from . import ansi
+
+ # Handle tabs
+ line = line.replace('\t', ' ' * tab_width)
+
+ if ansi.style_aware_wcswidth(line) == -1:
+ raise (ValueError("text contains an unprintable character"))
+
+ if max_width < 1:
+ raise ValueError("max_width must be at least 1")
+
+ if ansi.style_aware_wcswidth(line) > max_width:
+ # Remove characters until we fit. Leave room for the ellipsis.
+ line = line[:max_width - 1]
+ while ansi.style_aware_wcswidth(line) > max_width - 1:
+ line = line[:-1]
+
+ line += "\N{HORIZONTAL ELLIPSIS}"
+
+ return line
diff --git a/docs/api/utility_functions.rst b/docs/api/utility_functions.rst
index e083cafe..e2d6f036 100644
--- a/docs/api/utility_functions.rst
+++ b/docs/api/utility_functions.rst
@@ -17,6 +17,8 @@ Utility Functions
.. autofunction:: cmd2.utils.align_right
+.. autofunction:: cmd2.utils.truncate_line
+
.. autofunction:: cmd2.utils.strip_quotes
.. autofunction:: cmd2.utils.namedtuple_with_defaults
diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst
index f3d2a7f4..d5224e86 100644
--- a/docs/features/generating_output.rst
+++ b/docs/features/generating_output.rst
@@ -140,17 +140,19 @@ the terminal or not.
Aligning Text
--------------
-If you would like to generate output which is left, center, or right aligned within a
-specified width or the terminal width, the following functions can help:
+If you would like to generate output which is left, center, or right aligned
+within a specified width or the terminal width, the following functions can
+help:
- :meth:`cmd2.utils.align_left`
- :meth:`cmd2.utils.align_center`
- :meth:`cmd2.utils.align_right`
-These functions differ from Python's string justifying functions in that they support
-characters with display widths greater than 1. Additionally, ANSI style sequences are safely
-ignored and do not count toward the display width. This means colored text is supported. If
-text has line breaks, then each line is aligned independently.
+These functions differ from Python's string justifying functions in that they
+support characters with display widths greater than 1. Additionally, ANSI style
+sequences are safely ignored and do not count toward the display width. This
+means colored text is supported. If text has line breaks, then each line is
+aligned independently.
@@ -165,5 +167,5 @@ in the output to generate colors on the terminal.
The :meth:`cmd2.ansi.style_aware_wcswidth` function solves both of these
problems. Pass it a string, and regardless of which Unicode characters and ANSI
-text style escape sequences it contains, it will tell you how many characters on the
-screen that string will consume when printed.
+text style escape sequences it contains, it will tell you how many characters
+on the screen that string will consume when printed.
diff --git a/docs/features/settings.rst b/docs/features/settings.rst
index 627f61a9..697c68a7 100644
--- a/docs/features/settings.rst
+++ b/docs/features/settings.rst
@@ -70,8 +70,8 @@ echo
~~~~
If ``True``, each command the user issues will be repeated to the screen before
-it is executed. This is particularly useful when running scripts. This behavior
-does not occur when a running command at the prompt.
+it is executed. This is particularly useful when running scripts. This
+behavior does not occur when a running command at the prompt.
editor
@@ -105,13 +105,14 @@ Allow access to your application in one of the
max_completion_items
~~~~~~~~~~~~~~~~~~~~
-Maximum number of CompletionItems to display during tab completion. A CompletionItem
-is a special kind of tab-completion hint which displays both a value and description
-and uses one line for each hint. Tab complete the ``set`` command for an example.
+Maximum number of CompletionItems to display during tab completion. A
+CompletionItem is a special kind of tab-completion hint which displays both a
+value and description and uses one line for each hint. Tab complete the ``set``
+command for an example.
-If the number of tab-completion hints exceeds ``max_completion_items``, then they will
-be displayed in the typical columnized format and will not include the description text
-of the CompletionItem.
+If the number of tab-completion hints exceeds ``max_completion_items``, then
+they will be displayed in the typical columnized format and will not include
+the description text of the CompletionItem.
prompt
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b5231172..9dd54ee2 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -293,54 +293,113 @@ def test_context_flag_exit_err(context_flag):
context_flag.__exit__()
+def test_truncate_line():
+ line = 'long'
+ max_width = 3
+ truncated = cu.truncate_line(line, max_width)
+ assert truncated == 'lo\N{HORIZONTAL ELLIPSIS}'
+
+def test_truncate_line_with_newline():
+ line = 'fo\no'
+ max_width = 2
+ with pytest.raises(ValueError):
+ cu.truncate_line(line, max_width)
+
+def test_truncate_line_width_is_too_small():
+ line = 'foo'
+ max_width = 0
+ with pytest.raises(ValueError):
+ cu.truncate_line(line, max_width)
+
+def test_truncate_line_wide_text():
+ line = '苹苹other'
+ max_width = 6
+ truncated = cu.truncate_line(line, max_width)
+ assert truncated == '苹苹o\N{HORIZONTAL ELLIPSIS}'
+
+def test_truncate_line_split_wide_text():
+ """Test when truncation results in a string which is shorter than max_width"""
+ line = '1苹2苹'
+ max_width = 3
+ truncated = cu.truncate_line(line, max_width)
+ assert truncated == '1\N{HORIZONTAL ELLIPSIS}'
+
+def test_truncate_line_tabs():
+ line = 'has\ttab'
+ max_width = 9
+ truncated = cu.truncate_line(line, max_width)
+ assert truncated == 'has t\N{HORIZONTAL ELLIPSIS}'
+
def test_align_text_fill_char_is_tab():
text = 'foo'
fill_char = '\t'
width = 5
- aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
assert aligned == text + ' '
+def test_align_text_width_is_too_small():
+ text = 'foo'
+ fill_char = '-'
+ width = 0
+ with pytest.raises(ValueError):
+ cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
+
def test_align_text_fill_char_is_too_long():
text = 'foo'
fill_char = 'fill'
width = 5
with pytest.raises(TypeError):
- cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
def test_align_text_fill_char_is_unprintable():
text = 'foo'
fill_char = '\n'
width = 5
with pytest.raises(ValueError):
- cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
def test_align_text_has_tabs():
text = '\t\tfoo'
fill_char = '-'
width = 10
- aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT, tab_width=2)
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=2)
assert aligned == ' ' + 'foo' + '---'
def test_align_text_blank():
text = ''
fill_char = '-'
width = 5
- aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
assert aligned == fill_char * width
def test_align_text_wider_than_width():
- text = 'long'
+ text = 'long text field'
fill_char = '-'
- width = 3
- aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ width = 8
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
assert aligned == text
+def test_align_text_wider_than_width_truncate():
+ text = 'long text field'
+ fill_char = '-'
+ width = 8
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True)
+ assert aligned == 'long te\N{HORIZONTAL ELLIPSIS}'
+
+def test_align_text_wider_than_width_truncate_add_fill():
+ """Test when truncation results in a string which is shorter than width and align_text adds filler"""
+ text = '1苹2苹'
+ fill_char = '-'
+ width = 3
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True)
+ assert aligned == '1\N{HORIZONTAL ELLIPSIS}-'
+
def test_align_text_has_unprintable():
text = 'foo\x02'
fill_char = '-'
width = 5
with pytest.raises(ValueError):
- cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
def test_align_text_term_width():
import shutil
@@ -351,7 +410,7 @@ def test_align_text_term_width():
term_width = shutil.get_terminal_size().columns
expected_fill = (term_width - ansi.style_aware_wcswidth(text)) * fill_char
- aligned = cu.align_text(text, fill_char=fill_char, alignment=cu.TextAlignment.LEFT)
+ aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char)
assert aligned == text + expected_fill
def test_align_left():