Repository: pandas-dev/pandas

Issue:
Formatting Exception in io/formats/terminal.py
#### The Code

```python
from pandas import DataFrame
df = DataFrame([[1,2,3], [4,5,6]])
print df
```
#### What I get when I try to print "df":
```
Traceback (most recent call last):

  File "C:\Anaconda2\lib\site-packages\IPython\core\formatters.py", line 699, in __call__
    printer.pretty(obj)

  File "C:\Anaconda2\lib\site-packages\IPython\lib\pretty.py", line 403, in pretty
    return _repr_pprint(obj, self, cycle)

  File "C:\Anaconda2\lib\site-packages\IPython\lib\pretty.py", line 703, in _repr_pprint
    output = repr(obj)

  File "C:\Anaconda2\lib\site-packages\pandas\core\base.py", line 78, in __repr__
    return str(self)

  File "C:\Anaconda2\lib\site-packages\pandas\core\base.py", line 58, in __str__
    return self.__bytes__()

  File "C:\Anaconda2\lib\site-packages\pandas\core\base.py", line 70, in __bytes__
    return self.__unicode__().encode(encoding, 'replace')

  File "C:\Anaconda2\lib\site-packages\pandas\core\frame.py", line 633, in __unicode__
    line_width=width, show_dimensions=show_dimensions)

  File "C:\Anaconda2\lib\site-packages\pandas\core\frame.py", line 713, in to_string
    line_width=line_width)

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\format.py", line 428, in __init__
    self._chk_truncate()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\format.py", line 444, in _chk_truncate
    (w, h) = get_terminal_size()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\terminal.py", line 41, in get_terminal_size
    tuple_xy = _get_terminal_size_tput()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\terminal.py", line 103, in _get_terminal_size_tput
    cols = int(output[0])

ValueError: invalid literal for int() with base 10: ''

Traceback (most recent call last):

  File "C:\Anaconda2\lib\site-packages\IPython\core\formatters.py", line 338, in __call__
    return method()

  File "C:\Anaconda2\lib\site-packages\pandas\core\frame.py", line 667, in _repr_html_
    show_dimensions=show_dimensions, notebook=True)

  File "C:\Anaconda2\lib\site-packages\pandas\core\frame.py", line 2253, in to_html
    render_links=render_links)

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\format.py", line 428, in __init__
    self._chk_truncate()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\format.py", line 444, in _chk_truncate
    (w, h) = get_terminal_size()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\terminal.py", line 41, in get_terminal_size
    tuple_xy = _get_terminal_size_tput()

  File "C:\Anaconda2\lib\site-packages\pandas\io\formats\terminal.py", line 103, in _get_terminal_size_tput
    cols = int(output[0])

ValueError: invalid literal for int() with base 10: ''
```
#### Problem description
I get this exception when running the code quoted above in the iPython console in the latest version of Anaconda Spyder (3.3.2); the Python version is Anaconda 2.7.15.

Apparently, the ValueError has existed all along, but it was previously masked by the try/except block that wraps line 103 in terminal.py.

The function in 0.23 looked like this:
```python
def _get_terminal_size_tput():
    # get terminal width
    # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width
    # -height-of-a-terminal-window
    try:
        import subprocess
        proc = subprocess.Popen(["tput", "cols"],
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE)
        output = proc.communicate(input=None)
        cols = int(output[0])
        proc = subprocess.Popen(["tput", "lines"],
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE)
        output = proc.communicate(input=None)
        rows = int(output[0])
        return (cols, rows)
    except:
return None
```

In 0.24, it looks like this:
```python
def _get_terminal_size_tput():
    # get terminal width
    # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width
    # -height-of-a-terminal-window
    try:
        import subprocess
        proc = subprocess.Popen(["tput", "cols"],
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE)
        output = proc.communicate(input=None)
        cols = int(output[0])
        proc = subprocess.Popen(["tput", "lines"],
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE)
        output = proc.communicate(input=None)
        rows = int(output[0])
        return (cols, rows)
    except OSError:
        return None
```

The change here is in the "except" line:  it's gone from a blanket "except" (which is, admittedly, bad practice), to an "OSError".  I believe the "except" needs to look like this:

```python
    except (OSError, ValueError):
```

Yes, the underlying issue will still be there (it always has been), but nonetheless it displays just fine in my Spyder console window.  I'd submit a PR myself, but I'm not confident I know enough about the pandas codebase that my fix is what's needed.
#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 2.7.15.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: en
LOCALE: None.None

pandas: 0.24.0
pytest: 4.1.1
pip: 18.1
setuptools: 40.6.3
Cython: 0.29.2
numpy: 1.15.4
scipy: 1.2.0
pyarrow: None
xarray: None
IPython: 5.8.0
sphinx: 1.8.2
patsy: 0.5.1
dateutil: 2.7.5
pytz: 2018.9
blosc: None
bottleneck: 1.2.1
tables: 3.4.4
numexpr: 2.6.9
feather: None
matplotlib: 2.2.3
openpyxl: 2.5.12
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.2
lxml.etree: 4.3.0
bs4: 4.7.1
html5lib: 1.0.1
sqlalchemy: 1.2.16
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None

</details>



Discussion:
Rather than catching `OSError, ValueError` I think we should

1. Do the subprocess stuff in a try / except OSError
2. Do the parsing (`cols = int(output[0])`, etc.) in a try / except `ValueError`
Seems interesting. I'm a bit of a newbie to open source, but I wouldn't mind taking a look at it. Do I have your go-ahead?

Resolving patch:
diff --git a/pandas/io/formats/terminal.py b/pandas/io/formats/terminal.py
--- a/pandas/io/formats/terminal.py
+++ b/pandas/io/formats/terminal.py
@@ -15,6 +15,7 @@
 
 import os
 import shutil
+import subprocess
 
 from pandas.compat import PY3
 
@@ -94,22 +95,29 @@ def _get_terminal_size_tput():
     # get terminal width
     # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width
     # -height-of-a-terminal-window
+
     try:
-        import subprocess
         proc = subprocess.Popen(["tput", "cols"],
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE)
-        output = proc.communicate(input=None)
-        cols = int(output[0])
+        output_cols = proc.communicate(input=None)
         proc = subprocess.Popen(["tput", "lines"],
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE)
-        output = proc.communicate(input=None)
-        rows = int(output[0])
-        return (cols, rows)
+        output_rows = proc.communicate(input=None)
     except OSError:
         return None
 
+    try:
+        # Some terminals (e.g. spyder) may report a terminal size of '',
+        # making the `int` fail.
+
+        cols = int(output_cols[0])
+        rows = int(output_rows[0])
+        return cols, rows
+    except (ValueError, IndexError):
+        return None
+
 
 def _get_terminal_size_linux():
     def ioctl_GWINSZ(fd):


Test patch:
diff --git a/pandas/tests/io/formats/test_console.py b/pandas/tests/io/formats/test_console.py
--- a/pandas/tests/io/formats/test_console.py
+++ b/pandas/tests/io/formats/test_console.py
@@ -1,6 +1,9 @@
+import subprocess  # noqa: F401
+
 import pytest
 
 from pandas.io.formats.console import detect_console_encoding
+from pandas.io.formats.terminal import _get_terminal_size_tput
 
 
 class MockEncoding(object):  # TODO(py27): replace with mock
@@ -72,3 +75,19 @@ def test_detect_console_encoding_fallback_to_default(monkeypatch, std, locale):
         context.setattr('sys.stdout', MockEncoding(std))
         context.setattr('sys.getdefaultencoding', lambda: 'sysDefaultEncoding')
         assert detect_console_encoding() == 'sysDefaultEncoding'
+
+
+@pytest.mark.parametrize("size", ['', ['']])
+def test_terminal_unknown_dimensions(monkeypatch, size):
+    mock = pytest.importorskip("unittest.mock")
+
+    def communicate(*args, **kwargs):
+        return size
+
+    monkeypatch.setattr('subprocess.Popen', mock.Mock())
+    monkeypatch.setattr('subprocess.Popen.return_value.returncode', None)
+    monkeypatch.setattr(
+        'subprocess.Popen.return_value.communicate', communicate)
+    result = _get_terminal_size_tput()
+
+    assert result is None