Handle EPIPE/BrokenPipeError in pytest's CLI

Running `pytest | head -1` and similar causes an annoying error to be
printed to stderr:

    Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
    BrokenPipeError: [Errno 32] Broken pipe

(or possibly even a propagating exception in older/other Python versions).

The standard UNIX behavior is to handle the EPIPE silently. To
recommended method to do this in Python is described here:
https://docs.python.org/3/library/signal.html#note-on-sigpipe

It is not appropriate to apply this recommendation to `pytest.main()`,
which is used programmatically for in-process runs. Hence, change
pytest's entrypoint to a new `pytest.console_main()` function, to be
used exclusively by pytest's CLI, and add the SIGPIPE code there.

Fixes #4375.
This commit is contained in:
Ran Benita
2020-05-07 21:20:09 +03:00
parent de556f895f
commit 73448f265d
6 changed files with 45 additions and 3 deletions

View File

@@ -137,6 +137,24 @@ def main(args=None, plugins=None) -> Union[int, ExitCode]:
return ExitCode.USAGE_ERROR
def console_main() -> int:
"""pytest's CLI entry point.
This function is not meant for programmable use; use `main()` instead.
"""
# https://docs.python.org/3/library/signal.html#note-on-sigpipe
try:
code = main()
sys.stdout.flush()
return code
except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output
# to devnull to avoid another BrokenPipeError at shutdown
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
return 1 # Python exits with error code 1 on EPIPE
class cmdline: # compatibility namespace
main = staticmethod(main)

View File

@@ -6,6 +6,7 @@ from . import collect
from _pytest import __version__
from _pytest.assertion import register_assert_rewrite
from _pytest.config import cmdline
from _pytest.config import console_main
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config import hookspec
@@ -57,6 +58,7 @@ __all__ = [
"cmdline",
"collect",
"Collector",
"console_main",
"deprecated_call",
"exit",
"ExitCode",

View File

@@ -4,4 +4,4 @@ pytest entry point
import pytest
if __name__ == "__main__":
raise SystemExit(pytest.main())
raise SystemExit(pytest.console_main())