Skip to content

Commit

Permalink
Merge pull request #20473 from jitseniesen/format
Browse files Browse the repository at this point in the history
PR: Use .format() to format floats in array and dataframe editors
  • Loading branch information
ccordoba12 committed Feb 4, 2023
2 parents dc630ab + 6153bc2 commit 87e93db
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 111 deletions.
121 changes: 62 additions & 59 deletions spyder/plugins/variableexplorer/widgets/arrayeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,48 +41,48 @@
from spyder.utils.qthelpers import add_actions, create_action, keybinding
from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog

# Note: string and unicode data types will be formatted with '%s' (see below)
# Note: string and unicode data types will be formatted with 's' (see below)
SUPPORTED_FORMATS = {
'single': '%.6g',
'double': '%.6g',
'float_': '%.6g',
'longfloat': '%.6g',
'float16': '%.6g',
'float32': '%.6g',
'float64': '%.6g',
'float96': '%.6g',
'float128': '%.6g',
'csingle': '%r',
'complex_': '%r',
'clongfloat': '%r',
'complex64': '%r',
'complex128': '%r',
'complex192': '%r',
'complex256': '%r',
'byte': '%d',
'bytes8': '%s',
'short': '%d',
'intc': '%d',
'int_': '%d',
'longlong': '%d',
'intp': '%d',
'int8': '%d',
'int16': '%d',
'int32': '%d',
'int64': '%d',
'ubyte': '%d',
'ushort': '%d',
'uintc': '%d',
'uint': '%d',
'ulonglong': '%d',
'uintp': '%d',
'uint8': '%d',
'uint16': '%d',
'uint32': '%d',
'uint64': '%d',
'bool_': '%r',
'bool8': '%r',
'bool': '%r',
'single': '.6g',
'double': '.6g',
'float_': '.6g',
'longfloat': '.6g',
'float16': '.6g',
'float32': '.6g',
'float64': '.6g',
'float96': '.6g',
'float128': '.6g',
'csingle': '.6g',
'complex_': '.6g',
'clongfloat': '.6g',
'complex64': '.6g',
'complex128': '.6g',
'complex192': '.6g',
'complex256': '.6g',
'byte': 'd',
'bytes8': 's',
'short': 'd',
'intc': 'd',
'int_': 'd',
'longlong': 'd',
'intp': 'd',
'int8': 'd',
'int16': 'd',
'int32': 'd',
'int64': 'd',
'ubyte': 'd',
'ushort': 'd',
'uintc': 'd',
'uint': 'd',
'ulonglong': 'd',
'uintp': 'd',
'uint8': 'd',
'uint16': 'd',
'uint32': 'd',
'uint64': 'd',
'bool_': '',
'bool8': '',
'bool': '',
}


Expand Down Expand Up @@ -120,7 +120,7 @@ class ArrayModel(QAbstractTableModel):
ROWS_TO_LOAD = 500
COLS_TO_LOAD = 40

def __init__(self, data, format="%.6g", xlabels=None, ylabels=None,
def __init__(self, data, format_spec=".6g", xlabels=None, ylabels=None,
readonly=False, parent=None):
QAbstractTableModel.__init__(self)

Expand All @@ -145,7 +145,7 @@ def __init__(self, data, format="%.6g", xlabels=None, ylabels=None,
self.alp = .6 # Alpha-channel

self._data = data
self._format = format
self._format_spec = format_spec

self.total_rows = self._data.shape[0]
self.total_cols = self._data.shape[1]
Expand Down Expand Up @@ -192,18 +192,18 @@ def __init__(self, data, format="%.6g", xlabels=None, ylabels=None,
else:
self.cols_loaded = self.total_cols

def get_format(self):
def get_format_spec(self):
"""Return current format"""
# Avoid accessing the private attribute _format from outside
return self._format
# Avoid accessing the private attribute _format_spec from outside
return self._format_spec

def get_data(self):
"""Return data"""
return self._data

def set_format(self, format):
def set_format_spec(self, format_spec):
"""Change display format"""
self._format = format
self._format_spec = format_spec
self.reset()

def columnCount(self, qindex=QModelIndex()):
Expand Down Expand Up @@ -288,7 +288,8 @@ def data(self, index, role=Qt.DisplayRole):
return value_to_display(value)
else:
try:
return to_qvariant(self._format % value)
format_spec = self._format_spec
return to_qvariant(format(value, format_spec))
except TypeError:
self.readonly = True
return repr(value)
Expand Down Expand Up @@ -346,7 +347,8 @@ def setData(self, index, value, role=Qt.EditRole):
return False

# Add change to self.changes
self.changes[(i, j)] = val
# Use self.test_array to convert to correct dtype
self.changes[(i, j)] = self.test_array[0]
self.dataChanged.emit(index, index)

if not is_string(val):
Expand Down Expand Up @@ -559,8 +561,9 @@ def _sel_to_text(self, cell_range):
_data = self.model().get_data()
output = io.BytesIO()
try:
fmt = '%' + self.model().get_format_spec()
np.savetxt(output, _data[row_min:row_max+1, col_min:col_max+1],
delimiter='\t', fmt=self.model().get_format())
delimiter='\t', fmt=fmt)
except:
QMessageBox.warning(self, _("Warning"),
_("It was not possible to copy values for "
Expand Down Expand Up @@ -592,8 +595,8 @@ def __init__(self, parent, data, readonly=False,
self.old_data_shape = self.data.shape
self.data.shape = (1, 1)

format = SUPPORTED_FORMATS.get(data.dtype.name, '%s')
self.model = ArrayModel(self.data, format=format, xlabels=xlabels,
format_spec = SUPPORTED_FORMATS.get(data.dtype.name, 's')
self.model = ArrayModel(self.data, format_spec=format_spec, xlabels=xlabels,
ylabels=ylabels, readonly=readonly, parent=self)
self.view = ArrayView(self, self.model, data.dtype, data.shape)

Expand All @@ -616,18 +619,18 @@ def reject_changes(self):
@Slot()
def change_format(self):
"""Change display format"""
format, valid = QInputDialog.getText(self, _( 'Format'),
format_spec, valid = QInputDialog.getText(self, _( 'Format'),
_( "Float formatting"),
QLineEdit.Normal, self.model.get_format())
QLineEdit.Normal, self.model.get_format_spec())
if valid:
format = str(format)
format_spec = str(format_spec)
try:
format % 1.1
format(1.1, format_spec)
except:
QMessageBox.critical(self, _("Error"),
_("Format (%s) is incorrect") % format)
_("Format (%s) is incorrect") % format_spec)
return
self.model.set_format(format)
self.model.set_format_spec(format_spec)


class ArrayEditor(BaseDialog):
Expand Down
59 changes: 25 additions & 34 deletions spyder/plugins/variableexplorer/widgets/dataframeeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@
from qtpy.QtCore import (QAbstractTableModel, QModelIndex, Qt, Signal, Slot,
QItemSelectionModel, QEvent)
from qtpy.QtGui import QColor, QCursor
from qtpy.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
QHBoxLayout, QInputDialog, QLineEdit, QMenu,
QMessageBox, QPushButton, QTableView,
QScrollBar, QTableWidget, QFrame,
QItemDelegate)
from qtpy.QtWidgets import (QApplication, QCheckBox, QGridLayout, QHBoxLayout,
QInputDialog, QLineEdit, QMenu, QMessageBox,
QPushButton, QTableView, QScrollBar, QTableWidget,
QFrame, QItemDelegate)
from spyder_kernels.utils.lazymodules import numpy as np, pandas as pd

# Local imports
Expand All @@ -68,7 +67,7 @@
_bool_false = ['false', 'f', '0', '0.', '0.0', ' ']

# Default format for data frames with floats
DEFAULT_FORMAT = '%.6g'
DEFAULT_FORMAT = '.6g'

# Limit at which dataframe is considered so large that it is loaded on demand
LARGE_SIZE = 5e5
Expand Down Expand Up @@ -118,13 +117,13 @@ class DataFrameModel(QAbstractTableModel):
https://github.com/wavexx/gtabview/blob/master/gtabview/models.py
"""

def __init__(self, dataFrame, format=DEFAULT_FORMAT, parent=None):
def __init__(self, dataFrame, format_spec=DEFAULT_FORMAT, parent=None):
QAbstractTableModel.__init__(self)
self.dialog = parent
self.df = dataFrame
self.df_columns_list = None
self.df_index_list = None
self._format = format
self._format_spec = format_spec
self.complex_intran = None
self.display_error_idxs = []

Expand Down Expand Up @@ -265,14 +264,14 @@ def max_min_col_update(self):
max_min = None
self.max_min_col.append(max_min)

def get_format(self):
"""Return current format"""
# Avoid accessing the private attribute _format from outside
return self._format
def get_format_spec(self):
"""Return current format+spec"""
# Avoid accessing the private attribute _format_spec from outside
return self._format_spec

def set_format(self, format):
def set_format_spec(self, format_spec):
"""Change display format"""
self._format = format
self._format_spec = format_spec
self.reset()

def bgcolor(self, state):
Expand Down Expand Up @@ -354,11 +353,11 @@ def data(self, index, role=Qt.DisplayRole):
value = self.get_value(row, column)
if isinstance(value, float):
try:
return to_qvariant(self._format % value)
return to_qvariant(format(value, self._format_spec))
except (ValueError, TypeError):
# may happen if format = '%d' and value = NaN;
# may happen if format = 'd' and value = NaN;
# see spyder-ide/spyder#4139.
return to_qvariant(DEFAULT_FORMAT % value)
return to_qvariant(format(value, DEFAULT_FORMAT))
elif is_type_text_string(value):
# Don't perform any conversion on strings
# because it leads to differences between
Expand Down Expand Up @@ -1017,8 +1016,7 @@ def setup_and_check(self, data, title=''):
self.setModel(self.dataModel)
self.resizeColumnsToContents()

format = '%' + self.get_conf('dataframe_format')
self.dataModel.set_format(format)
self.dataModel.set_format_spec(self.get_conf('dataframe_format'))

return True

Expand Down Expand Up @@ -1301,26 +1299,19 @@ def change_format(self):
"""
Ask user for display format for floats and use it.
"""
format, valid = QInputDialog.getText(self, _('Format'),
_("Float formatting"),
QLineEdit.Normal,
self.dataModel.get_format())
format_spec, valid = QInputDialog.getText(
self, _('Format'), _("Float formatting"), QLineEdit.Normal,
self.dataModel.get_format_spec())
if valid:
format = str(format)
format_spec = str(format_spec)
try:
format % 1.1
format(1.1, format_spec)
except:
msg = _("Format ({}) is incorrect").format(format)
msg = _("Format ({}) is incorrect").format(format_spec)
QMessageBox.critical(self, _("Error"), msg)
return
if not format.startswith('%'):
msg = _("Format ({}) should start with '%'").format(format)
QMessageBox.critical(self, _("Error"), msg)
return
self.dataModel.set_format(format)

format = format[1:]
self.set_conf('dataframe_format', format)
self.dataModel.set_format_spec(format_spec)
self.set_conf('dataframe_format', format_spec)

def get_value(self):
"""Return modified Dataframe -- this is *not* a copy"""
Expand Down
15 changes: 13 additions & 2 deletions spyder/plugins/variableexplorer/widgets/tests/test_arrayeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,25 @@ def test_arrayeditor_format(setup_arrayeditor, qtbot):
qtbot.keyClick(dlg.arraywidget.view, Qt.Key_Down, modifier=Qt.ShiftModifier)
contents = dlg.arraywidget.view._sel_to_text(dlg.arraywidget.view.selectedIndexes())
assert contents == "1\n2\n"
dlg.arraywidget.view.model().set_format("%.18e")
assert dlg.arraywidget.view.model().get_format() == "%.18e"
dlg.arraywidget.view.model().set_format_spec(".18e")
assert dlg.arraywidget.view.model().get_format_spec() == ".18e"
qtbot.keyClick(dlg.arraywidget.view, Qt.Key_Down, modifier=Qt.ShiftModifier)
qtbot.keyClick(dlg.arraywidget.view, Qt.Key_Down, modifier=Qt.ShiftModifier)
contents = dlg.arraywidget.view._sel_to_text(dlg.arraywidget.view.selectedIndexes())
assert contents == "1.000000000000000000e+00\n2.000000000000000000e+00\n"


@pytest.mark.parametrize(
'data',
[np.array([10000])]
)
def test_arrayeditor_format_thousands(setup_arrayeditor):
"""Check that format can include thousands separator."""
model = setup_arrayeditor.arraywidget.model
model.set_format_spec(',.2f')
assert model.data(model.index(0, 0)) == '10,000.00'


def test_arrayeditor_with_inf_array(qtbot, recwarn):
"""See: spyder-ide/spyder#8093"""
arr = np.array([np.inf])
Expand Down
Loading

0 comments on commit 87e93db

Please sign in to comment.