Skip to content

Commit 1f67279

Browse files
committed
BUG: Auto-detect terminal size on max_col/max_row==0
1 parent 1a2885f commit 1f67279

File tree

4 files changed

+144
-38
lines changed

4 files changed

+144
-38
lines changed

doc/source/v0.15.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -786,3 +786,5 @@ Bug Fixes
786786
needed interpolating (:issue:`7173`).
787787
- Bug where ``col_space`` was ignored in ``DataFrame.to_string()`` when ``header=False``
788788
(:issue:`8230`).
789+
790+
- Bug in DataFrame terminal display: Setting max_column/max_rows to zero did not trigger auto-resizing of dfs to fit terminal width/height (:issue:`7180`).

pandas/core/config_init.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,30 @@
3333

3434
pc_max_rows_doc = """
3535
: int
36-
This sets the maximum number of rows pandas should output when printing
37-
out various output. For example, this value determines whether the repr()
38-
for a dataframe prints out fully or just a summary repr.
39-
'None' value means unlimited.
36+
If max_rows is exceeded, switch to truncate view. Depending on
37+
`large_repr`, objects are either centrally truncated or printed as
38+
a summary view. 'None' value means unlimited.
39+
40+
In case python/IPython is running in a terminal and `large_repr`
41+
equals 'truncate' this can be set to 0 and pandas will auto-detect
42+
the height of the terminal and print a truncated object which fits
43+
the screen height. The IPython notebook, IPython qtconsole, or
44+
IDLE do not run in a terminal and hence it is not possible to do
45+
correct auto-detection.
4046
"""
4147

4248
pc_max_cols_doc = """
4349
: int
44-
max_rows and max_columns are used in __repr__() methods to decide if
45-
to_string() or info() is used to render an object to a string. In case
46-
python/IPython is running in a terminal this can be set to 0 and pandas
47-
will correctly auto-detect the width the terminal and swap to a smaller
48-
format in case all columns would not fit vertically. The IPython notebook,
49-
IPython qtconsole, or IDLE do not run in a terminal and hence it is not
50-
possible to do correct auto-detection.
51-
'None' value means unlimited.
50+
If max_cols is exceeded, switch to truncate view. Depending on
51+
`large_repr`, objects are either centrally truncated or printed as
52+
a summary view. 'None' value means unlimited.
53+
54+
In case python/IPython is running in a terminal and `large_repr`
55+
equals 'truncate' this can be set to 0 and pandas will auto-detect
56+
the width of the terminal and print a truncated object which fits
57+
the screen width. The IPython notebook, IPython qtconsole, or IDLE
58+
do not run in a terminal and hence it is not possible to do
59+
correct auto-detection.
5260
"""
5361

5462
pc_max_levels_doc = """

pandas/core/format.py

+92-26
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
# pylint: disable=W0141
55

66
import sys
7-
import re
87

98
from pandas.core.base import PandasObject
10-
from pandas.core.common import adjoin, isnull, notnull
9+
from pandas.core.common import adjoin, notnull
1110
from pandas.core.index import Index, MultiIndex, _ensure_index
1211
from pandas import compat
1312
from pandas.compat import(StringIO, lzip, range, map, zip, reduce, u,
1413
OrderedDict)
1514
from pandas.util.terminal import get_terminal_size
16-
from pandas.core.config import get_option, set_option, reset_option
15+
from pandas.core.config import get_option, set_option
1716
import pandas.core.common as com
1817
import pandas.lib as lib
1918
from pandas.tslib import iNaT
@@ -22,7 +21,6 @@
2221

2322
import itertools
2423
import csv
25-
from datetime import time
2624

2725
from pandas.tseries.period import PeriodIndex, DatetimeIndex
2826

@@ -321,30 +319,69 @@ def __init__(self, frame, buf=None, columns=None, col_space=None,
321319
self._chk_truncate()
322320

323321
def _chk_truncate(self):
322+
'''
323+
Checks whether the frame should be truncated. If so, slices
324+
the frame up.
325+
'''
324326
from pandas.tools.merge import concat
325327

326-
truncate_h = self.max_cols and (len(self.columns) > self.max_cols)
327-
truncate_v = self.max_rows and (len(self.frame) > self.max_rows)
328+
# Column of which first element is used to determine width of a dot col
329+
self.tr_size_col = -1
328330

329331
# Cut the data to the information actually printed
330332
max_cols = self.max_cols
331333
max_rows = self.max_rows
334+
335+
if max_cols == 0 or max_rows == 0: # assume we are in the terminal (why else = 0)
336+
(w,h) = get_terminal_size()
337+
self.w = w
338+
self.h = h
339+
if self.max_rows == 0:
340+
dot_row = 1
341+
prompt_row = 1
342+
if self.show_dimensions:
343+
show_dimension_rows = 3
344+
n_add_rows = self.header + dot_row + show_dimension_rows + prompt_row
345+
max_rows_adj = self.h - n_add_rows # rows available to fill with actual data
346+
self.max_rows_adj = max_rows_adj
347+
348+
# Format only rows and columns that could potentially fit the screen
349+
if max_cols == 0 and len(self.frame.columns) > w:
350+
max_cols = w
351+
if max_rows == 0 and len(self.frame) > h:
352+
max_rows = h
353+
354+
if not hasattr(self,'max_rows_adj'):
355+
self.max_rows_adj = max_rows
356+
if not hasattr(self,'max_cols_adj'):
357+
self.max_cols_adj = max_cols
358+
359+
max_cols_adj = self.max_cols_adj
360+
max_rows_adj = self.max_rows_adj
361+
362+
truncate_h = max_cols_adj and (len(self.columns) > max_cols_adj)
363+
truncate_v = max_rows_adj and (len(self.frame) > max_rows_adj)
364+
332365
frame = self.frame
333366
if truncate_h:
334-
if max_cols > 1:
335-
col_num = (max_cols // 2)
336-
frame = concat( (frame.iloc[:,:col_num],frame.iloc[:,-col_num:]),axis=1 )
337-
else:
338-
col_num = max_cols
367+
if max_cols_adj == 0:
368+
col_num = len(frame.columns)
369+
elif max_cols_adj == 1:
339370
frame = frame.iloc[:,:max_cols]
371+
col_num = max_cols
372+
else:
373+
col_num = (max_cols_adj // 2)
374+
frame = concat( (frame.iloc[:,:col_num],frame.iloc[:,-col_num:]),axis=1 )
340375
self.tr_col_num = col_num
341376
if truncate_v:
342-
if max_rows > 1:
343-
row_num = max_rows // 2
344-
frame = concat( (frame.iloc[:row_num,:],frame.iloc[-row_num:,:]) )
345-
else:
377+
if max_rows_adj == 0:
378+
row_num = len(frame)
379+
if max_rows_adj == 1:
346380
row_num = max_rows
347381
frame = frame.iloc[:max_rows,:]
382+
else:
383+
row_num = max_rows_adj // 2
384+
frame = concat( (frame.iloc[:row_num,:],frame.iloc[-row_num:,:]) )
348385
self.tr_row_num = row_num
349386

350387
self.tr_frame = frame
@@ -360,13 +397,12 @@ def _to_str_columns(self):
360397
frame = self.tr_frame
361398

362399
# may include levels names also
363-
str_index = self._get_formatted_index(frame)
364400

401+
str_index = self._get_formatted_index(frame)
365402
str_columns = self._get_formatted_column_labels(frame)
366403

367404
if self.header:
368405
stringified = []
369-
col_headers = frame.columns
370406
for i, c in enumerate(frame):
371407
cheader = str_columns[i]
372408
max_colwidth = max(self.col_space or 0,
@@ -389,7 +425,6 @@ def _to_str_columns(self):
389425
else:
390426
stringified = []
391427
for i, c in enumerate(frame):
392-
formatter = self._get_formatter(i)
393428
fmt_values = self._format_col(i)
394429
fmt_values = _make_fixed_width(fmt_values, self.justify,
395430
minimum=(self.col_space or 0))
@@ -406,8 +441,8 @@ def _to_str_columns(self):
406441

407442
if truncate_h:
408443
col_num = self.tr_col_num
409-
col_width = len(strcols[col_num][0]) # infer from column header
410-
strcols.insert(col_num + 1, ['...'.center(col_width)] * (len(str_index)))
444+
col_width = len(strcols[self.tr_size_col][0]) # infer from column header
445+
strcols.insert(self.tr_col_num + 1, ['...'.center(col_width)] * (len(str_index)))
411446
if truncate_v:
412447
n_header_rows = len(str_index) - len(frame)
413448
row_num = self.tr_row_num
@@ -424,19 +459,19 @@ def _to_str_columns(self):
424459
if ix == 0:
425460
dot_str = my_str.ljust(cwidth)
426461
elif is_dot_col:
462+
cwidth = len(strcols[self.tr_size_col][0])
427463
dot_str = my_str.center(cwidth)
428464
else:
429465
dot_str = my_str.rjust(cwidth)
430466

431467
strcols[ix].insert(row_num + n_header_rows, dot_str)
432-
433468
return strcols
434469

435470
def to_string(self):
436471
"""
437472
Render a DataFrame to a console-friendly tabular output.
438473
"""
439-
474+
from pandas import Series
440475
frame = self.frame
441476

442477
if len(frame.columns) == 0 or len(frame.index) == 0:
@@ -447,10 +482,40 @@ def to_string(self):
447482
text = info_line
448483
else:
449484
strcols = self._to_str_columns()
450-
if self.line_width is None:
485+
if self.line_width is None: # no need to wrap around just print the whole frame
451486
text = adjoin(1, *strcols)
452-
else:
487+
elif not isinstance(self.max_cols,int) or self.max_cols > 0: # perhaps need to wrap around
453488
text = self._join_multiline(*strcols)
489+
else: # max_cols == 0. Try to fit frame to terminal
490+
text = adjoin(1, *strcols).split('\n')
491+
row_lens = Series(text).apply(len)
492+
max_len_col_ix = np.argmax(row_lens)
493+
max_len = row_lens[max_len_col_ix]
494+
headers = [ele[0] for ele in strcols]
495+
# Size of last col determines dot col size. See `self._to_str_columns
496+
size_tr_col = len(headers[self.tr_size_col])
497+
max_len += size_tr_col # Need to make space for largest row plus truncate (dot) col
498+
dif = max_len - self.w
499+
adj_dif = dif
500+
col_lens = Series([Series(ele).apply(len).max() for ele in strcols])
501+
n_cols = len(col_lens)
502+
counter = 0
503+
while adj_dif > 0 and n_cols > 1:
504+
counter += 1
505+
mid = int(round(n_cols / 2.))
506+
mid_ix = col_lens.index[mid]
507+
col_len = col_lens[mid_ix]
508+
adj_dif -= ( col_len + 1 ) # adjoin adds one
509+
col_lens = col_lens.drop(mid_ix)
510+
n_cols = len(col_lens)
511+
max_cols_adj = n_cols - self.index # subtract index column
512+
self.max_cols_adj = max_cols_adj
513+
514+
# Call again _chk_truncate to cut frame appropriately
515+
# and then generate string representation
516+
self._chk_truncate()
517+
strcols = self._to_str_columns()
518+
text = adjoin(1, *strcols)
454519

455520
self.buf.writelines(text)
456521

@@ -472,8 +537,8 @@ def _join_multiline(self, *strcols):
472537
col_bins = _binify(col_widths, lwidth)
473538
nbins = len(col_bins)
474539

475-
if self.max_rows and len(self.frame) > self.max_rows:
476-
nrows = self.max_rows + 1
540+
if self.truncate_v:
541+
nrows = self.max_rows_adj + 1
477542
else:
478543
nrows = len(self.frame)
479544

@@ -636,6 +701,7 @@ def is_numeric_dtype(dtype):
636701
for x in str_columns:
637702
x.append('')
638703

704+
# self.str_columns = str_columns
639705
return str_columns
640706

641707
@property

pandas/tests/test_format.py

+30
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,36 @@ def mkframe(n):
280280
com.pprint_thing(df._repr_fits_horizontal_())
281281
self.assertTrue(has_expanded_repr(df))
282282

283+
def test_auto_detect(self):
284+
term_width, term_height = get_terminal_size()
285+
fac = 1.05 # Arbitrary large factor to exceed term widht
286+
cols = range(int(term_width * fac))
287+
index = range(10)
288+
df = DataFrame(index=index, columns=cols)
289+
with option_context('mode.sim_interactive', True):
290+
with option_context('max_rows',None):
291+
with option_context('max_columns',None):
292+
# Wrap around with None
293+
self.assertTrue(has_expanded_repr(df))
294+
with option_context('max_rows',0):
295+
with option_context('max_columns',0):
296+
# Truncate with auto detection.
297+
self.assertTrue(has_horizontally_truncated_repr(df))
298+
299+
index = range(int(term_height * fac))
300+
df = DataFrame(index=index, columns=cols)
301+
with option_context('max_rows',0):
302+
with option_context('max_columns',None):
303+
# Wrap around with None
304+
self.assertTrue(has_expanded_repr(df))
305+
# Truncate vertically
306+
self.assertTrue(has_vertically_truncated_repr(df))
307+
308+
with option_context('max_rows',None):
309+
with option_context('max_columns',0):
310+
self.assertTrue(has_horizontally_truncated_repr(df))
311+
312+
283313
def test_to_string_repr_unicode(self):
284314
buf = StringIO()
285315

0 commit comments

Comments
 (0)