Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Use .format() to format floats in array and dataframe editors #20473

Merged
merged 5 commits into from
Feb 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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