diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3a44b0260153c..d1ede31fd5d1d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -749,6 +749,7 @@ I/O - :func:`read_sas()` will parse numbers in sas7bdat-files that have width less than 8 bytes correctly. (:issue:`21616`) - :func:`read_sas()` will correctly parse sas7bdat files with many columns (:issue:`22628`) - :func:`read_sas()` will correctly parse sas7bdat files with data page types having also bit 7 set (so page type is 128 + 256 = 384) (:issue:`16615`) +- Bug in :meth:`detect_client_encoding` where potential ``IOError`` goes unhandled when importing in a mod_wsgi process due to restricted access to stdout. (:issue:`21552`) Plotting ^^^^^^^^ diff --git a/pandas/io/formats/console.py b/pandas/io/formats/console.py index 45d50ea3fa073..b8b28a0b0c98c 100644 --- a/pandas/io/formats/console.py +++ b/pandas/io/formats/console.py @@ -21,7 +21,7 @@ def detect_console_encoding(): encoding = None try: encoding = sys.stdout.encoding or sys.stdin.encoding - except AttributeError: + except (AttributeError, IOError): pass # try again for something better diff --git a/pandas/tests/io/formats/test_console.py b/pandas/tests/io/formats/test_console.py new file mode 100644 index 0000000000000..055763bf62d6e --- /dev/null +++ b/pandas/tests/io/formats/test_console.py @@ -0,0 +1,74 @@ +import pytest + +from pandas.io.formats.console import detect_console_encoding + + +class MockEncoding(object): # TODO(py27): replace with mock + """ + Used to add a side effect when accessing the 'encoding' property. If the + side effect is a str in nature, the value will be returned. Otherwise, the + side effect should be an exception that will be raised. + """ + def __init__(self, encoding): + super(MockEncoding, self).__init__() + self.val = encoding + + @property + def encoding(self): + return self.raise_or_return(self.val) + + @staticmethod + def raise_or_return(val): + if isinstance(val, str): + return val + else: + raise val + + +@pytest.mark.parametrize('empty,filled', [ + ['stdin', 'stdout'], + ['stdout', 'stdin'] +]) +def test_detect_console_encoding_from_stdout_stdin(monkeypatch, empty, filled): + # Ensures that when sys.stdout.encoding or sys.stdin.encoding is used when + # they have values filled. + # GH 21552 + with monkeypatch.context() as context: + context.setattr('sys.{}'.format(empty), MockEncoding('')) + context.setattr('sys.{}'.format(filled), MockEncoding(filled)) + assert detect_console_encoding() == filled + + +@pytest.mark.parametrize('encoding', [ + AttributeError, + IOError, + 'ascii' +]) +def test_detect_console_encoding_fallback_to_locale(monkeypatch, encoding): + # GH 21552 + with monkeypatch.context() as context: + context.setattr('locale.getpreferredencoding', lambda: 'foo') + context.setattr('sys.stdout', MockEncoding(encoding)) + assert detect_console_encoding() == 'foo' + + +@pytest.mark.parametrize('std,locale', [ + ['ascii', 'ascii'], + ['ascii', Exception], + [AttributeError, 'ascii'], + [AttributeError, Exception], + [IOError, 'ascii'], + [IOError, Exception] +]) +def test_detect_console_encoding_fallback_to_default(monkeypatch, std, locale): + # When both the stdout/stdin encoding and locale preferred encoding checks + # fail (or return 'ascii', we should default to the sys default encoding. + # GH 21552 + with monkeypatch.context() as context: + context.setattr( + 'locale.getpreferredencoding', + lambda: MockEncoding.raise_or_return(locale) + ) + context.setattr('sys.stdout', MockEncoding(std)) + context.setattr('sys.getdefaultencoding', lambda: 'sysDefaultEncoding') + assert detect_console_encoding() == 'sysDefaultEncoding'