From 379a9e8bffc9a93678d6a94520e830a6b14a949b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Wed, 21 Oct 2026 23:27:24 +0800 Subject: [PATCH] gh-160043: Don't reject fitting text in textwrap.shorten()/wrap(max_lines) The placeholder-fits guard ran eagerly whenever max_lines was set, so shorten() and wrap() raised ValueError for text that fits in width when the placeholder was wider than width. Move the check to the branch that force-places a placeholder-only line, where the placeholder is actually used. --- Lib/test/test_textwrap.py | 14 ++++++++++++++ Lib/textwrap.py | 11 ++++------- .../2026-10-21-23-20-22.gh-issue-160043.AlgGSI.rst | 4 ++++ 3 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-21-23-20-22.gh-issue-160043.AlgGSI.rst diff --git a/Lib/test/test_textwrap.py b/Lib/test/test_textwrap.py index aca1f427656bb5..13289b5a491a2e 100644 --- a/Lib/test/test_textwrap.py +++ b/Lib/test/test_textwrap.py @@ -559,6 +559,20 @@ def test_placeholder(self): placeholder=' [truncated]...') self.check_wrap(self.text, 80, [self.text], placeholder='.' * 1000) + def test_placeholder_not_needed_when_text_fits(self): + # With max_lines set, text that fits must be returned as is even when + # the placeholder would not fit in the width; the placeholder is only + # required when the text is actually truncated. + self.assertEqual(shorten('ab', width=4), 'ab') + self.assertEqual(shorten('a', width=1), 'a') + self.assertEqual( + wrap('hello world', width=20, max_lines=3, + subsequent_indent=' ' * 17), + ['hello world']) + # Truncation that cannot place the placeholder still raises. + with self.assertRaises(ValueError): + shorten('a very long text goes here', width=4) + def test_placeholder_backtrack(self): # Test special case when max_lines insufficient, but what # would be last wrapped line so long the placeholder cannot diff --git a/Lib/textwrap.py b/Lib/textwrap.py index 2f213e34c2c329..9476656f3361b7 100644 --- a/Lib/textwrap.py +++ b/Lib/textwrap.py @@ -251,13 +251,6 @@ def _wrap_chunks(self, chunks): lines = [] if self.width <= 0: raise ValueError("invalid width %r (must be > 0)" % self.width) - if self.max_lines is not None: - if self.max_lines > 1: - indent = self.subsequent_indent - else: - indent = self.initial_indent - if len(indent) + len(self.placeholder.lstrip()) > self.width: - raise ValueError("placeholder too large for max width") # Arrange in reverse order so items can be efficiently popped # from a stack of chucks. @@ -333,6 +326,10 @@ def _wrap_chunks(self, chunks): self.width): lines[-1] = prev_line + self.placeholder break + if (len(indent) + len(self.placeholder.lstrip()) + > self.width): + raise ValueError( + "placeholder too large for max width") lines.append(indent + self.placeholder.lstrip()) break diff --git a/Misc/NEWS.d/next/Library/2026-10-21-23-20-22.gh-issue-160043.AlgGSI.rst b/Misc/NEWS.d/next/Library/2026-10-21-23-20-22.gh-issue-160043.AlgGSI.rst new file mode 100644 index 00000000000000..10ba97651ef83b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-21-23-20-22.gh-issue-160043.AlgGSI.rst @@ -0,0 +1,4 @@ +:func:`textwrap.shorten` and :func:`textwrap.wrap` with *max_lines* no longer +raise :exc:`ValueError` for text that fits in *width* when the *placeholder* is +wider than *width*. The placeholder is only required when the text is actually +truncated.