Skip to content

Commit 2d7a94d

Browse files
committed
handle broken pipe
1 parent ae15db6 commit 2d7a94d

File tree

2 files changed

+61
-6
lines changed

2 files changed

+61
-6
lines changed

rich/console.py

Lines changed: 40 additions & 6 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,
@@ -1989,6 +1996,20 @@ def log(
19891996
):
19901997
buffer_extend(line)
19911998

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+
19922013
def _check_buffer(self) -> None:
19932014
"""Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
19942015
Rendering is supported on Windows, Unix and Jupyter environments. For
@@ -1998,6 +2019,19 @@ def _check_buffer(self) -> None:
19982019
if self.quiet:
19992020
del self._buffer[:]
20002021
return
2022+
2023+
try:
2024+
self._write_buffer()
2025+
except BrokenPipeError:
2026+
self.on_broken_pipe()
2027+
2028+
def _write_buffer(self) -> None:
2029+
"""Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
2030+
Rendering is supported on Windows, Unix and Jupyter environments. For
2031+
legacy Windows consoles, the win32 API is called directly.
2032+
This method will also record what it renders if recording is enabled via Console.record.
2033+
"""
2034+
20012035
with self._lock:
20022036
if self.record:
20032037
with self._record_buffer_lock:

tests/test_console.py

Lines changed: 21 additions & 0 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
@@ -1017,3 +1018,23 @@ def test_reenable_highlighting() -> None:
10171018
lines[1]
10181019
== "\x1b[1m[\x1b[0m\x1b[1;36m1\x1b[0m, \x1b[1;36m2\x1b[0m, \x1b[1;36m3\x1b[0m\x1b[1m]\x1b[0m"
10191020
)
1021+
1022+
1023+
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
1024+
def test_brokenpipeerror() -> None:
1025+
"""Test BrokenPipe works as expected."""
1026+
which_py, which_head = (["which", cmd] for cmd in ("python", "head"))
1027+
rich_cmd = "python -m rich".split()
1028+
for cmd in [which_py, which_head, rich_cmd]:
1029+
check = subprocess.run(cmd).returncode
1030+
if check != 0:
1031+
return # Only test on suitable Unix platforms
1032+
head_cmd = "head -1".split()
1033+
proc1 = subprocess.Popen(rich_cmd, stdout=subprocess.PIPE)
1034+
proc2 = subprocess.Popen(head_cmd, stdin=proc1.stdout, stdout=subprocess.PIPE)
1035+
proc1.stdout.close()
1036+
output, _ = proc2.communicate()
1037+
proc1.wait()
1038+
proc2.wait()
1039+
assert proc1.returncode == 1
1040+
assert proc2.returncode == 0

0 commit comments

Comments
 (0)