Skip to content

Commit 0c0ba7f

Browse files
committed
gh-160633: Break the last chunk of a broken long word in textwrap truncation
1 parent 1159836 commit 0c0ba7f

4 files changed

Lines changed: 56 additions & 1 deletion

File tree

‎Doc/library/textwrap.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ functions should be good enough; otherwise, you should use an instance of
6666
>>> textwrap.shorten("Hello world", width=10, placeholder="...")
6767
'Hello...'
6868

69+
If no words can be kept before the *placeholder* (for example, when
70+
*text* is a single word longer than *width*) and *break_long_words* is
71+
true, the word is broken so that part of it is kept before the
72+
*placeholder*.
73+
6974
Optional keyword arguments correspond to the instance attributes of
7075
:class:`TextWrapper`, documented below. Note that the whitespace is
7176
collapsed before the text is passed to the :class:`TextWrapper` :meth:`fill`

‎Lib/test/test_textwrap.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,28 @@ def test_max_lines_long(self):
636636
['Did you say',
637637
'"supercalifr',
638638
'agilisticexp',
639-
'[...]'],
639+
'ialido [...]'],
640640
max_lines=4)
641641

642+
def test_max_lines_long_word_broken(self):
643+
# A long word broken across lines is broken further on the last
644+
# line to make room for the placeholder, instead of dropping the
645+
# whole remaining piece.
646+
self.check_wrap('x' * 30, 10,
647+
['xxxxxxxxxx', 'xxxx [...]'],
648+
max_lines=2)
649+
self.check_wrap('x' * 30, 10,
650+
['xxxx [...]'],
651+
max_lines=1)
652+
# Not enough room for the placeholder plus any text: the
653+
# placeholder-only line is kept.
654+
self.check_wrap('x' * 30, 5,
655+
['xxxxx', '[...]'],
656+
max_lines=2)
657+
self.check_wrap('x' * 30, 10,
658+
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
659+
max_lines=2, break_long_words=False)
660+
642661

643662
class LongWordWithHyphensTestCase(BaseTestCase):
644663
def setUp(self):
@@ -1132,6 +1151,16 @@ def test_width_too_small_for_placeholder(self):
11321151
def test_first_word_too_long_but_placeholder_fits(self):
11331152
self.check_shorten("Helloo", 5, "[...]")
11341153

1154+
def test_long_word_broken_to_fit_placeholder(self):
1155+
# A single word longer than the width is broken further to make
1156+
# room for the placeholder, instead of being replaced by it alone.
1157+
self.check_shorten("x" * 30, 10, "xxxx [...]")
1158+
self.check_shorten("x" * 30, 20, "xxxxxxxxxxxxxx [...]")
1159+
# Not enough room for the placeholder plus any text.
1160+
self.check_shorten("x" * 30, 5, "[...]")
1161+
# Words are only broken when break_long_words is true.
1162+
self.check_shorten("x" * 30, 10, "[...]", break_long_words=False)
1163+
11351164

11361165
if __name__ == '__main__':
11371166
unittest.main()

‎Lib/textwrap.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,23 @@ def _wrap_chunks(self, chunks):
324324
cur_line.append(self.placeholder)
325325
lines.append(indent + ''.join(cur_line))
326326
break
327+
if (self.break_long_words and
328+
len(cur_line) == 1 and
329+
cur_line[-1].strip() and
330+
len(cur_line[-1]) == width and
331+
width - len(self.placeholder) > 0):
332+
# The placeholder does not fit after this line,
333+
# and the line consists of a single full-width
334+
# chunk: a piece of a long word broken by
335+
# _handle_long_word(), or a word exactly as wide
336+
# as the line. Break it further instead of
337+
# dropping it whole, or the last line (and, for
338+
# shorten(), the whole text) would be replaced
339+
# by the placeholder alone.
340+
space_left = width - len(self.placeholder)
341+
cur_line[-1] = cur_line[-1][:space_left]
342+
cur_len = space_left
343+
continue
327344
cur_len -= len(cur_line[-1])
328345
del cur_line[-1]
329346
else:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`textwrap.shorten` and :func:`textwrap.wrap` with ``max_lines``
2+
discarding a broken long word -- and with it, for :func:`textwrap.shorten`,
3+
all remaining text -- instead of breaking it further to make room for the
4+
placeholder.

0 commit comments

Comments
 (0)