From d98e1729b3e1e83d796727d59be610dae4254611 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sun, 16 Aug 2015 14:00:31 -0500 Subject: [PATCH 1/2] Handle EPIPE IOErrors when using more than 1 job If someone is using flake8 and piping it to a command like `head`, the command they are piping flake8's output too may close the pipe earlier than flake8 expects. To avoid extraneous exception output being printed, we now catch IOErrors and check their errnos to ensure they're something we know we can ignore. This also provides flexibility to add further errnos for ignoring on a case-by-case basis. Closes #69 --- flake8/reporter.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/flake8/reporter.py b/flake8/reporter.py index 5fc2743..1df3d9e 100644 --- a/flake8/reporter.py +++ b/flake8/reporter.py @@ -2,6 +2,7 @@ # Adapted from a contribution of Johan Dahlin import collections +import errno import re import sys try: @@ -18,6 +19,20 @@ class BaseQReport(pep8.BaseReport): """Base Queue Report.""" _loaded = False # Windows support + # Reasoning for ignored error numbers is in-line below + ignored_errors = set([ + # EPIPE: Added by sigmavirus24 + # > If output during processing is piped to something that may close + # > its own stdin before we've finished printing results, we need to + # > catch a Broken pipe error and continue on. + # > (See also: https://gitlab.com/pycqa/flake8/issues/69) + errno.EPIPE, + # NOTE(sigmavirus24): When adding to this list, include the reasoning + # on the lines before the error code and always append your error + # code. Further, please always add a trailing `,` to reduce the visual + # noise in diffs. + ]) + def __init__(self, options): assert options.jobs > 0 super(BaseQReport, self).__init__(options) @@ -74,6 +89,11 @@ class BaseQReport(pep8.BaseReport): self._process_main() except KeyboardInterrupt: pass + except IOError as ioerr: + # If we happen across an IOError that we aren't certain can/should + # be ignored, we should re-raise the exception. + if ioerr.errno not in self.ignored_errors: + raise finally: # ensure all output is flushed before main process continues sys.stdout.flush() From 1ed78df61ebe32f6d1edac490e5d07e8c0558451 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Tue, 18 Aug 2015 20:02:58 -0500 Subject: [PATCH 2/2] Add a regression test for EPIPE IOErrors This should prevent bug 69 from regressing in the future and provides a framework for testing the addition of new errnos to the ingore list. --- flake8/tests/test_reporter.py | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 flake8/tests/test_reporter.py diff --git a/flake8/tests/test_reporter.py b/flake8/tests/test_reporter.py new file mode 100644 index 0000000..f91bb52 --- /dev/null +++ b/flake8/tests/test_reporter.py @@ -0,0 +1,36 @@ +from __future__ import with_statement + +import errno +import unittest +try: + from unittest import mock +except ImportError: + import mock # < PY33 + +from flake8 import reporter + + +def ioerror_report_factory(errno_code): + class IOErrorBaseQReport(reporter.BaseQReport): + def _process_main(self): + raise IOError(errno_code, 'Fake bad pipe exception') + + options = mock.MagicMock() + options.jobs = 2 + return IOErrorBaseQReport(options) + + +class TestBaseQReport(unittest.TestCase): + def test_does_not_raise_a_bad_pipe_ioerror(self): + """Test that no EPIPE IOError exception is re-raised or leaked.""" + report = ioerror_report_factory(errno.EPIPE) + try: + report.process_main() + except IOError: + self.fail('BaseQReport.process_main raised an IOError for EPIPE' + ' but it should have caught this exception.') + + def test_raises_a_enoent_ioerror(self): + """Test that an ENOENT IOError exception is re-raised.""" + report = ioerror_report_factory(errno.ENOENT) + self.assertRaises(IOError, report.process_main)