Skip to content

Commit 5e4a6a2

Browse files
committed
Fixed bug where rich_utils._apply_style_wrapper() could strip a trailing newline.
1 parent fd72525 commit 5e4a6a2

File tree

2 files changed

+76
-22
lines changed

2 files changed

+76
-22
lines changed

cmd2/rich_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,9 @@ def _apply_style_wrapper(cls: type[Segment], *args: Any, **kwargs: Any) -> Itera
416416
styled_segments = list(_orig_segment_apply_style(*args, **kwargs))
417417
newline_segment = cls.line()
418418

419-
# If the final segment is a newline, it will be stripped by Segment.split_lines().
419+
# If the final segment ends in a newline, that newline will be stripped by Segment.split_lines().
420420
# Save an unstyled newline to restore later.
421-
end_segment = newline_segment if styled_segments and styled_segments[-1].text == "\n" else None
421+
end_segment = newline_segment if styled_segments and styled_segments[-1].text.endswith("\n") else None
422422

423423
# Use Segment.split_lines() to separate the styled text from the newlines.
424424
# This way the ANSI reset code will appear before any newline.

tests/test_rich_utils.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -147,101 +147,106 @@ def test_from_ansi_wrapper() -> None:
147147

148148
@pytest.mark.parametrize(
149149
# Print with style and verify that everything but newline characters have style.
150-
('objects', 'expected', 'sep', 'end'),
150+
('objects', 'sep', 'end', 'expected'),
151151
[
152152
# Print nothing
153-
((), "\n", " ", "\n"),
153+
((), " ", "\n", "\n"),
154154
# Empty string
155-
(("",), "\n", " ", "\n"),
155+
(("",), " ", "\n", "\n"),
156156
# Multple empty strings
157-
(("", ""), '\x1b[34;47m \x1b[0m\n', " ", "\n"),
157+
(("", ""), " ", "\n", "\x1b[34;47m \x1b[0m\n"),
158158
# Basic string
159159
(
160160
("str_1",),
161-
"\x1b[34;47mstr_1\x1b[0m\n",
162161
" ",
163162
"\n",
163+
"\x1b[34;47mstr_1\x1b[0m\n",
164164
),
165165
# String which ends with newline
166166
(
167167
("str_1\n",),
168-
"\x1b[34;47mstr_1\x1b[0m\n\n",
169168
" ",
170169
"\n",
170+
"\x1b[34;47mstr_1\x1b[0m\n\n",
171171
),
172172
# String which ends with multiple newlines
173173
(
174174
("str_1\n\n",),
175-
"\x1b[34;47mstr_1\x1b[0m\n\n\n",
176175
" ",
177176
"\n",
177+
"\x1b[34;47mstr_1\x1b[0m\n\n\n",
178178
),
179179
# Mutiple lines
180180
(
181181
("str_1\nstr_2",),
182-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
183182
" ",
184183
"\n",
184+
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
185185
),
186186
# Multiple strings
187187
(
188188
("str_1", "str_2"),
189-
"\x1b[34;47mstr_1 str_2\x1b[0m\n",
190189
" ",
191190
"\n",
191+
"\x1b[34;47mstr_1 str_2\x1b[0m\n",
192192
),
193193
# Multiple strings with newline between them.
194194
(
195195
("str_1\n", "str_2"),
196-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47m str_2\x1b[0m\n",
197196
" ",
198197
"\n",
198+
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47m str_2\x1b[0m\n",
199199
),
200200
# Multiple strings and non-space value for sep
201201
(
202202
("str_1", "str_2"),
203-
"\x1b[34;47mstr_1(sep)str_2\x1b[0m\n",
204203
"(sep)",
205204
"\n",
205+
"\x1b[34;47mstr_1(sep)str_2\x1b[0m\n",
206206
),
207207
# Multiple strings and sep is a newline
208208
(
209209
("str_1", "str_2"),
210-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
211210
"\n",
212211
"\n",
212+
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
213213
),
214214
# Multiple strings and sep has newlines
215215
(
216216
("str_1", "str_2"),
217-
"\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)str_2\x1b[0m\n",
218-
"(sep1)\n(sep2)",
217+
"(sep1)\n(sep2)\n",
219218
"\n",
219+
("\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n"),
220220
),
221221
# Non-newline value for end.
222222
(
223223
("str_1", "str_2"),
224-
"\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)str_2\x1b[0m\x1b[34;47m(end)\x1b[0m",
225224
"(sep1)\n(sep2)",
226225
"(end)",
226+
"\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)str_2\x1b[0m\x1b[34;47m(end)\x1b[0m",
227227
),
228228
# end has newlines.
229229
(
230230
("str_1", "str_2"),
231-
"\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)str_2\x1b[0m\x1b[34;47m(end1)\x1b[0m\n\x1b[34;47m(end2)\x1b[0m",
232-
"(sep1)\n(sep2)",
233-
"(end1)\n(end2)",
231+
"(sep1)\n(sep2)\n",
232+
"(end1)\n(end2)\n",
233+
(
234+
"\x1b[34;47mstr_1(sep1)\x1b[0m\n"
235+
"\x1b[34;47m(sep2)\x1b[0m\n"
236+
"\x1b[34;47mstr_2\x1b[0m\x1b[34;47m(end1)\x1b[0m\n"
237+
"\x1b[34;47m(end2)\x1b[0m\n"
238+
),
234239
),
235240
# Empty sep and end values
236241
(
237242
("str_1", "str_2"),
238-
"\x1b[34;47mstr_1str_2\x1b[0m",
239243
"",
240244
"",
245+
"\x1b[34;47mstr_1str_2\x1b[0m",
241246
),
242247
],
243248
)
244-
def test_apply_style_wrapper(objects: tuple[str], expected: str, sep: str, end: str) -> None:
249+
def test_apply_style_wrapper_soft_wrap(objects: tuple[str], sep: str, end: str, expected: str) -> None:
245250
# Check if we are still patching Segment.apply_style(). If this check fails, then Rich
246251
# has fixed the bug. Therefore, we can remove this test function and ru._apply_style_wrapper.
247252
assert Segment.apply_style.__func__ is ru._apply_style_wrapper.__func__ # type: ignore[attr-defined]
@@ -275,3 +280,52 @@ def test_apply_style_wrapper(objects: tuple[str], expected: str, sep: str, end:
275280
finally:
276281
# Restore the patch
277282
Segment.apply_style = ru._apply_style_wrapper # type: ignore[assignment]
283+
284+
285+
def test_apply_style_wrapper_word_wrap() -> None:
286+
"""
287+
Test that our patch didn't mess up word wrapping.
288+
Make sure it does not insert styled newlines or apply style to existing newlines.
289+
"""
290+
# Check if we are still patching Segment.apply_style(). If this check fails, then Rich
291+
# has fixed the bug. Therefore, we can remove this test function and ru._apply_style_wrapper.
292+
assert Segment.apply_style.__func__ is ru._apply_style_wrapper.__func__ # type: ignore[attr-defined]
293+
294+
str1 = "this\nwill word wrap\n"
295+
str2 = "and\nso will this\n"
296+
sep = "(sep1)\n(sep2)\n"
297+
end = "(end1)\n(end2)\n"
298+
style = "blue on white"
299+
300+
# All newlines should appear outside of ANSI style sequences.
301+
expected = (
302+
"\x1b[34;47mthis\x1b[0m\n"
303+
"\x1b[34;47mwill word \x1b[0m\n"
304+
"\x1b[34;47mwrap\x1b[0m\n"
305+
"\x1b[34;47m(sep1)\x1b[0m\n"
306+
"\x1b[34;47m(sep2)\x1b[0m\n"
307+
"\x1b[34;47mand\x1b[0m\n"
308+
"\x1b[34;47mso will \x1b[0m\n"
309+
"\x1b[34;47mthis\x1b[0m\n"
310+
"\x1b[34;47m(end1)\x1b[0m\n"
311+
"\x1b[34;47m(end2)\x1b[0m\n"
312+
)
313+
314+
# Set a width which will cause word wrapping.
315+
console = Console(force_terminal=True, width=10)
316+
317+
try:
318+
with console.capture() as capture:
319+
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=False)
320+
assert capture.get() == expected
321+
322+
# Now remove our patch and make sure it produced the same result as unpatched Rich.
323+
Segment.apply_style = ru._orig_segment_apply_style # type: ignore[assignment]
324+
325+
with console.capture() as capture:
326+
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=False)
327+
assert capture.get() == expected
328+
329+
finally:
330+
# Restore the patch
331+
Segment.apply_style = ru._apply_style_wrapper # type: ignore[assignment]

0 commit comments

Comments
 (0)