From 1f672798fc25e864427ec410663db52a4d9cea7a Mon Sep 17 00:00:00 2001 From: benjamin Date: Mon, 7 Jul 2014 01:14:32 +0200 Subject: [PATCH] BUG: Auto-detect terminal size on max_col/max_row==0 --- doc/source/v0.15.0.txt | 2 + pandas/core/config_init.py | 32 ++++++---- pandas/core/format.py | 118 ++++++++++++++++++++++++++++-------- pandas/tests/test_format.py | 30 +++++++++ 4 files changed, 144 insertions(+), 38 deletions(-) diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index dd71ef1f63d54..d8e87ceaa830c 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -786,3 +786,5 @@ Bug Fixes needed interpolating (:issue:`7173`). - Bug where ``col_space`` was ignored in ``DataFrame.to_string()`` when ``header=False`` (:issue:`8230`). + +- 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`). diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index c32796cf082d4..8379266533c86 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -33,22 +33,30 @@ pc_max_rows_doc = """ : int - This sets the maximum number of rows pandas should output when printing - out various output. For example, this value determines whether the repr() - for a dataframe prints out fully or just a summary repr. - 'None' value means unlimited. + If max_rows is exceeded, switch to truncate view. Depending on + `large_repr`, objects are either centrally truncated or printed as + a summary view. 'None' value means unlimited. + + In case python/IPython is running in a terminal and `large_repr` + equals 'truncate' this can be set to 0 and pandas will auto-detect + the height of the terminal and print a truncated object which fits + the screen height. The IPython notebook, IPython qtconsole, or + IDLE do not run in a terminal and hence it is not possible to do + correct auto-detection. """ pc_max_cols_doc = """ : int - max_rows and max_columns are used in __repr__() methods to decide if - to_string() or info() is used to render an object to a string. In case - python/IPython is running in a terminal this can be set to 0 and pandas - will correctly auto-detect the width the terminal and swap to a smaller - format in case all columns would not fit vertically. The IPython notebook, - IPython qtconsole, or IDLE do not run in a terminal and hence it is not - possible to do correct auto-detection. - 'None' value means unlimited. + If max_cols is exceeded, switch to truncate view. Depending on + `large_repr`, objects are either centrally truncated or printed as + a summary view. 'None' value means unlimited. + + In case python/IPython is running in a terminal and `large_repr` + equals 'truncate' this can be set to 0 and pandas will auto-detect + the width of the terminal and print a truncated object which fits + the screen width. The IPython notebook, IPython qtconsole, or IDLE + do not run in a terminal and hence it is not possible to do + correct auto-detection. """ pc_max_levels_doc = """ diff --git a/pandas/core/format.py b/pandas/core/format.py index 2658410358000..89fe7b9b9a769 100644 --- a/pandas/core/format.py +++ b/pandas/core/format.py @@ -4,16 +4,15 @@ # pylint: disable=W0141 import sys -import re from pandas.core.base import PandasObject -from pandas.core.common import adjoin, isnull, notnull +from pandas.core.common import adjoin, notnull from pandas.core.index import Index, MultiIndex, _ensure_index from pandas import compat from pandas.compat import(StringIO, lzip, range, map, zip, reduce, u, OrderedDict) from pandas.util.terminal import get_terminal_size -from pandas.core.config import get_option, set_option, reset_option +from pandas.core.config import get_option, set_option import pandas.core.common as com import pandas.lib as lib from pandas.tslib import iNaT @@ -22,7 +21,6 @@ import itertools import csv -from datetime import time from pandas.tseries.period import PeriodIndex, DatetimeIndex @@ -321,30 +319,69 @@ def __init__(self, frame, buf=None, columns=None, col_space=None, self._chk_truncate() def _chk_truncate(self): + ''' + Checks whether the frame should be truncated. If so, slices + the frame up. + ''' from pandas.tools.merge import concat - truncate_h = self.max_cols and (len(self.columns) > self.max_cols) - truncate_v = self.max_rows and (len(self.frame) > self.max_rows) + # Column of which first element is used to determine width of a dot col + self.tr_size_col = -1 # Cut the data to the information actually printed max_cols = self.max_cols max_rows = self.max_rows + + if max_cols == 0 or max_rows == 0: # assume we are in the terminal (why else = 0) + (w,h) = get_terminal_size() + self.w = w + self.h = h + if self.max_rows == 0: + dot_row = 1 + prompt_row = 1 + if self.show_dimensions: + show_dimension_rows = 3 + n_add_rows = self.header + dot_row + show_dimension_rows + prompt_row + max_rows_adj = self.h - n_add_rows # rows available to fill with actual data + self.max_rows_adj = max_rows_adj + + # Format only rows and columns that could potentially fit the screen + if max_cols == 0 and len(self.frame.columns) > w: + max_cols = w + if max_rows == 0 and len(self.frame) > h: + max_rows = h + + if not hasattr(self,'max_rows_adj'): + self.max_rows_adj = max_rows + if not hasattr(self,'max_cols_adj'): + self.max_cols_adj = max_cols + + max_cols_adj = self.max_cols_adj + max_rows_adj = self.max_rows_adj + + truncate_h = max_cols_adj and (len(self.columns) > max_cols_adj) + truncate_v = max_rows_adj and (len(self.frame) > max_rows_adj) + frame = self.frame if truncate_h: - if max_cols > 1: - col_num = (max_cols // 2) - frame = concat( (frame.iloc[:,:col_num],frame.iloc[:,-col_num:]),axis=1 ) - else: - col_num = max_cols + if max_cols_adj == 0: + col_num = len(frame.columns) + elif max_cols_adj == 1: frame = frame.iloc[:,:max_cols] + col_num = max_cols + else: + col_num = (max_cols_adj // 2) + frame = concat( (frame.iloc[:,:col_num],frame.iloc[:,-col_num:]),axis=1 ) self.tr_col_num = col_num if truncate_v: - if max_rows > 1: - row_num = max_rows // 2 - frame = concat( (frame.iloc[:row_num,:],frame.iloc[-row_num:,:]) ) - else: + if max_rows_adj == 0: + row_num = len(frame) + if max_rows_adj == 1: row_num = max_rows frame = frame.iloc[:max_rows,:] + else: + row_num = max_rows_adj // 2 + frame = concat( (frame.iloc[:row_num,:],frame.iloc[-row_num:,:]) ) self.tr_row_num = row_num self.tr_frame = frame @@ -360,13 +397,12 @@ def _to_str_columns(self): frame = self.tr_frame # may include levels names also - str_index = self._get_formatted_index(frame) + str_index = self._get_formatted_index(frame) str_columns = self._get_formatted_column_labels(frame) if self.header: stringified = [] - col_headers = frame.columns for i, c in enumerate(frame): cheader = str_columns[i] max_colwidth = max(self.col_space or 0, @@ -389,7 +425,6 @@ def _to_str_columns(self): else: stringified = [] for i, c in enumerate(frame): - formatter = self._get_formatter(i) fmt_values = self._format_col(i) fmt_values = _make_fixed_width(fmt_values, self.justify, minimum=(self.col_space or 0)) @@ -406,8 +441,8 @@ def _to_str_columns(self): if truncate_h: col_num = self.tr_col_num - col_width = len(strcols[col_num][0]) # infer from column header - strcols.insert(col_num + 1, ['...'.center(col_width)] * (len(str_index))) + col_width = len(strcols[self.tr_size_col][0]) # infer from column header + strcols.insert(self.tr_col_num + 1, ['...'.center(col_width)] * (len(str_index))) if truncate_v: n_header_rows = len(str_index) - len(frame) row_num = self.tr_row_num @@ -424,19 +459,19 @@ def _to_str_columns(self): if ix == 0: dot_str = my_str.ljust(cwidth) elif is_dot_col: + cwidth = len(strcols[self.tr_size_col][0]) dot_str = my_str.center(cwidth) else: dot_str = my_str.rjust(cwidth) strcols[ix].insert(row_num + n_header_rows, dot_str) - return strcols def to_string(self): """ Render a DataFrame to a console-friendly tabular output. """ - + from pandas import Series frame = self.frame if len(frame.columns) == 0 or len(frame.index) == 0: @@ -447,10 +482,40 @@ def to_string(self): text = info_line else: strcols = self._to_str_columns() - if self.line_width is None: + if self.line_width is None: # no need to wrap around just print the whole frame text = adjoin(1, *strcols) - else: + elif not isinstance(self.max_cols,int) or self.max_cols > 0: # perhaps need to wrap around text = self._join_multiline(*strcols) + else: # max_cols == 0. Try to fit frame to terminal + text = adjoin(1, *strcols).split('\n') + row_lens = Series(text).apply(len) + max_len_col_ix = np.argmax(row_lens) + max_len = row_lens[max_len_col_ix] + headers = [ele[0] for ele in strcols] + # Size of last col determines dot col size. See `self._to_str_columns + size_tr_col = len(headers[self.tr_size_col]) + max_len += size_tr_col # Need to make space for largest row plus truncate (dot) col + dif = max_len - self.w + adj_dif = dif + col_lens = Series([Series(ele).apply(len).max() for ele in strcols]) + n_cols = len(col_lens) + counter = 0 + while adj_dif > 0 and n_cols > 1: + counter += 1 + mid = int(round(n_cols / 2.)) + mid_ix = col_lens.index[mid] + col_len = col_lens[mid_ix] + adj_dif -= ( col_len + 1 ) # adjoin adds one + col_lens = col_lens.drop(mid_ix) + n_cols = len(col_lens) + max_cols_adj = n_cols - self.index # subtract index column + self.max_cols_adj = max_cols_adj + + # Call again _chk_truncate to cut frame appropriately + # and then generate string representation + self._chk_truncate() + strcols = self._to_str_columns() + text = adjoin(1, *strcols) self.buf.writelines(text) @@ -472,8 +537,8 @@ def _join_multiline(self, *strcols): col_bins = _binify(col_widths, lwidth) nbins = len(col_bins) - if self.max_rows and len(self.frame) > self.max_rows: - nrows = self.max_rows + 1 + if self.truncate_v: + nrows = self.max_rows_adj + 1 else: nrows = len(self.frame) @@ -636,6 +701,7 @@ def is_numeric_dtype(dtype): for x in str_columns: x.append('') + # self.str_columns = str_columns return str_columns @property diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index d07065aed4b6a..5783d148df75d 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -280,6 +280,36 @@ def mkframe(n): com.pprint_thing(df._repr_fits_horizontal_()) self.assertTrue(has_expanded_repr(df)) + def test_auto_detect(self): + term_width, term_height = get_terminal_size() + fac = 1.05 # Arbitrary large factor to exceed term widht + cols = range(int(term_width * fac)) + index = range(10) + df = DataFrame(index=index, columns=cols) + with option_context('mode.sim_interactive', True): + with option_context('max_rows',None): + with option_context('max_columns',None): + # Wrap around with None + self.assertTrue(has_expanded_repr(df)) + with option_context('max_rows',0): + with option_context('max_columns',0): + # Truncate with auto detection. + self.assertTrue(has_horizontally_truncated_repr(df)) + + index = range(int(term_height * fac)) + df = DataFrame(index=index, columns=cols) + with option_context('max_rows',0): + with option_context('max_columns',None): + # Wrap around with None + self.assertTrue(has_expanded_repr(df)) + # Truncate vertically + self.assertTrue(has_vertically_truncated_repr(df)) + + with option_context('max_rows',None): + with option_context('max_columns',0): + self.assertTrue(has_horizontally_truncated_repr(df)) + + def test_to_string_repr_unicode(self): buf = StringIO()