Skip to content

Commit d6abebd

Browse files
authored
Merge branch 'master' into dataclasses_3.13
2 parents 848fdde + 1b2dada commit d6abebd

File tree

9 files changed

+226
-101
lines changed

9 files changed

+226
-101
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Fixed cached hash preservation upon clearing meta and links https://github.com/Textualize/rich/issues/2942
1919
- Fixed overriding the `background_color` of `Syntax` not including padding https://github.com/Textualize/rich/issues/3295
2020
- Fixed pretty printing of dataclasses with a default repr in Python 3.13 https://github.com/Textualize/rich/pull/3455
21+
- Fixed selective enabling of highlighting when disabled in the `Console` https://github.com/Textualize/rich/issues/3419
22+
- Fixed BrokenPipeError writing an error message https://github.com/Textualize/rich/pull/3468
23+
- Fixed superfluous space above Markdown tables https://github.com/Textualize/rich/pull/3469
24+
- Fixed issue with record and capture interaction https://github.com/Textualize/rich/pull/3470
25+
- Fixed control codes breaking in `append_tokens` https://github.com/Textualize/rich/pull/3471
2126

2227
### Changed
2328

@@ -31,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3136

3237
- Adds a `case_sensitive` parameter to `prompt.Prompt`. This determines if the
3338
response is treated as case-sensitive. Defaults to `True`.
39+
- Added `Console.on_broken_pipe` https://github.com/Textualize/rich/pull/3468
3440

3541
## [13.7.1] - 2024-02-28
3642

poetry.lock

Lines changed: 59 additions & 72 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ show_error_codes = true
5757
strict = true
5858
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
5959

60+
6061
[[tool.mypy.overrides]]
6162
module = ["pygments.*", "IPython.*", "ipywidgets.*"]
6263
ignore_missing_imports = true

rich/console.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,9 +1385,14 @@ def render_lines(
13851385
extra_lines = render_options.height - len(lines)
13861386
if extra_lines > 0:
13871387
pad_line = [
1388-
[Segment(" " * render_options.max_width, style), Segment("\n")]
1389-
if new_lines
1390-
else [Segment(" " * render_options.max_width, style)]
1388+
(
1389+
[
1390+
Segment(" " * render_options.max_width, style),
1391+
Segment("\n"),
1392+
]
1393+
if new_lines
1394+
else [Segment(" " * render_options.max_width, style)]
1395+
)
13911396
]
13921397
lines.extend(pad_line * extra_lines)
13931398

@@ -1436,9 +1441,11 @@ def render_str(
14361441
rich_text.overflow = overflow
14371442
else:
14381443
rich_text = Text(
1439-
_emoji_replace(text, default_variant=self._emoji_variant)
1440-
if emoji_enabled
1441-
else text,
1444+
(
1445+
_emoji_replace(text, default_variant=self._emoji_variant)
1446+
if emoji_enabled
1447+
else text
1448+
),
14421449
justify=justify,
14431450
overflow=overflow,
14441451
style=style,
@@ -1535,7 +1542,11 @@ def check_text() -> None:
15351542
if isinstance(renderable, str):
15361543
append_text(
15371544
self.render_str(
1538-
renderable, emoji=emoji, markup=markup, highlighter=_highlighter
1545+
renderable,
1546+
emoji=emoji,
1547+
markup=markup,
1548+
highlight=highlight,
1549+
highlighter=_highlighter,
15391550
)
15401551
)
15411552
elif isinstance(renderable, Text):
@@ -1985,6 +1996,20 @@ def log(
19851996
):
19861997
buffer_extend(line)
19871998

1999+
def on_broken_pipe(self) -> None:
2000+
"""This function is called when a `BrokenPipeError` is raised.
2001+
2002+
This can occur when piping Textual output in Linux and macOS.
2003+
The default implementation is to exit the app, but you could implement
2004+
this method in a subclass to change the behavior.
2005+
2006+
See https://docs.python.org/3/library/signal.html#note-on-sigpipe for details.
2007+
"""
2008+
self.quiet = True
2009+
devnull = os.open(os.devnull, os.O_WRONLY)
2010+
os.dup2(devnull, sys.stdout.fileno())
2011+
raise SystemExit(1)
2012+
19882013
def _check_buffer(self) -> None:
19892014
"""Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
19902015
Rendering is supported on Windows, Unix and Jupyter environments. For
@@ -1994,8 +2019,17 @@ def _check_buffer(self) -> None:
19942019
if self.quiet:
19952020
del self._buffer[:]
19962021
return
2022+
2023+
try:
2024+
self._write_buffer()
2025+
except BrokenPipeError:
2026+
self.on_broken_pipe()
2027+
2028+
def _write_buffer(self) -> None:
2029+
"""Write the buffer to the output file."""
2030+
19972031
with self._lock:
1998-
if self.record:
2032+
if self.record and not self._buffer_index:
19992033
with self._record_buffer_lock:
20002034
self._record_buffer.extend(self._buffer[:])
20012035

rich/markdown.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ def __rich_console__(
677677
and context.stack.top.on_child_close(context, element)
678678
)
679679
if should_render:
680-
if new_line:
680+
if new_line and node_type != "inline":
681681
yield _new_line_segment
682682
yield from console.render(element, context.options)
683683

rich/text.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ def append_tokens(
10411041
_Span = Span
10421042
offset = len(self)
10431043
for content, style in tokens:
1044+
content = strip_control_codes(content)
10441045
append_text(content)
10451046
if style:
10461047
append_span(_Span(offset, offset + len(content), style))

tests/test_console.py

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import io
33
import os
4+
import subprocess
45
import sys
56
import tempfile
67
from typing import Optional, Tuple, Type, Union
@@ -380,23 +381,6 @@ def test_capture():
380381
assert capture.get() == "Hello\n"
381382

382383

383-
def test_capture_and_record(capsys):
384-
recorder = Console(record=True)
385-
recorder.print("ABC")
386-
387-
with recorder.capture() as capture:
388-
recorder.print("Hello")
389-
390-
assert capture.get() == "Hello\n"
391-
392-
recorded_text = recorder.export_text()
393-
out, err = capsys.readouterr()
394-
395-
assert recorded_text == "ABC\nHello\n"
396-
assert capture.get() == "Hello\n"
397-
assert out == "ABC\n"
398-
399-
400384
def test_input(monkeypatch, capsys):
401385
def fake_input(prompt=""):
402386
console.file.write(prompt)
@@ -991,3 +975,70 @@ def test_force_color():
991975
},
992976
)
993977
assert console.color_system in ("truecolor", "windows")
978+
979+
980+
def test_reenable_highlighting() -> None:
981+
"""Check that when highlighting is disabled, it can be reenabled in print()"""
982+
console = Console(
983+
file=io.StringIO(),
984+
_environ={
985+
"FORCE_COLOR": "1",
986+
"TERM": "xterm-256color",
987+
"COLORTERM": "truecolor",
988+
},
989+
highlight=False,
990+
)
991+
console.print("[1, 2, 3]")
992+
console.print("[1, 2, 3]", highlight=True)
993+
output = console.file.getvalue()
994+
lines = output.splitlines()
995+
print(repr(lines))
996+
# First line not highlighted
997+
assert lines[0] == "[1, 2, 3]"
998+
# Second line highlighted
999+
1000+
assert (
1001+
lines[1]
1002+
== "\x1b[1m[\x1b[0m\x1b[1;36m1\x1b[0m, \x1b[1;36m2\x1b[0m, \x1b[1;36m3\x1b[0m\x1b[1m]\x1b[0m"
1003+
)
1004+
1005+
1006+
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
1007+
def test_brokenpipeerror() -> None:
1008+
"""Test BrokenPipe works as expected."""
1009+
which_py, which_head = (["which", cmd] for cmd in ("python", "head"))
1010+
rich_cmd = "python -m rich".split()
1011+
for cmd in [which_py, which_head, rich_cmd]:
1012+
check = subprocess.run(cmd).returncode
1013+
if check != 0:
1014+
return # Only test on suitable Unix platforms
1015+
head_cmd = "head -1".split()
1016+
proc1 = subprocess.Popen(rich_cmd, stdout=subprocess.PIPE)
1017+
proc2 = subprocess.Popen(head_cmd, stdin=proc1.stdout, stdout=subprocess.PIPE)
1018+
proc1.stdout.close()
1019+
output, _ = proc2.communicate()
1020+
proc1.wait()
1021+
proc2.wait()
1022+
assert proc1.returncode == 1
1023+
assert proc2.returncode == 0
1024+
1025+
1026+
def test_capture_and_record() -> None:
1027+
"""Regression test for https://github.com/Textualize/rich/issues/2563"""
1028+
1029+
console = Console(record=True)
1030+
print("Before Capture started:")
1031+
console.print("[blue underline]Print 0")
1032+
with console.capture() as capture:
1033+
console.print("[blue underline]Print 1")
1034+
console.print("[blue underline]Print 2")
1035+
console.print("[blue underline]Print 3")
1036+
console.print("[blue underline]Print 4")
1037+
1038+
capture_content = capture.get()
1039+
print(repr(capture_content))
1040+
assert capture_content == "Print 1\nPrint 2\nPrint 3\nPrint 4\n"
1041+
1042+
recorded_content = console.export_text()
1043+
print(repr(recorded_content))
1044+
assert recorded_content == "Print 0\n"

0 commit comments

Comments
 (0)