Repository: spyder-ide/spyder

Issue:
Test array builder widget



Discussion:

Resolving patch:
diff --git a/runtests.py b/runtests.py
new file mode 100644
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2009- The Spyder Development Team
+# Licensed under the terms of the MIT License
+#
+
+"""
+File for running tests programmatically.
+"""
+
+# Third party imports
+import qtpy
+import pytest
+
+
+def main():
+    """
+    Run pytest tests.
+    """
+    pytest.main(['-x', 'spyderlib',  '-v', '-rw', '--durations=10',
+                 '--cov=spyderlib', '--cov-report=term-missing'])
+
+
+if __name__ == '__main__':
+    main()
diff --git a/spyderlib/utils/fixtures.py b/spyderlib/utils/fixtures.py
new file mode 100644
--- /dev/null
+++ b/spyderlib/utils/fixtures.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2009- The Spyder Development Team
+# Licensed under the terms of the MIT License
+#
+
+"""
+Testing utilities to be used with pytest.
+"""
+
+# Standard library imports
+import shutil
+import tempfile
+
+# Third party imports
+import pytest
+
+# Local imports
+from spyderlib.config.user import UserConfig
+from spyderlib.config.main import CONF_VERSION, DEFAULTS
+
+
+@pytest.fixture
+def tmpconfig(request):
+    """
+    Fixtures that returns a temporary CONF element.
+    """
+    SUBFOLDER = tempfile.mkdtemp()
+    CONF = UserConfig('spyder-test',
+                      defaults=DEFAULTS,
+                      version=CONF_VERSION,
+                      subfolder=SUBFOLDER,
+                      raw_mode=True,
+                      )
+
+    def fin():
+        """
+        Fixture finalizer to delete the temporary CONF element.
+        """
+        shutil.rmtree(SUBFOLDER)
+
+    request.addfinalizer(fin)
+    return CONF
diff --git a/spyderlib/widgets/arraybuilder.py b/spyderlib/widgets/arraybuilder.py
--- a/spyderlib/widgets/arraybuilder.py
+++ b/spyderlib/widgets/arraybuilder.py
@@ -5,7 +5,7 @@
 # (see spyderlib/__init__.py for details)
 
 """
-Numpy Matrix/Array Builder Widget
+Numpy Matrix/Array Builder Widget.
 """
 
 # TODO:
@@ -39,13 +39,14 @@
 
 
 class NumpyArrayInline(QLineEdit):
-    """ """
     def __init__(self, parent):
         QLineEdit.__init__(self, parent)
         self._parent = parent
 
     def keyPressEvent(self, event):
-        """ """
+        """
+        Qt override.
+        """
         if event.key() in [Qt.Key_Enter, Qt.Key_Return]:
             self._parent.process_text()
             if self._parent.is_valid():
@@ -55,6 +56,11 @@ def keyPressEvent(self, event):
 
     # to catch the Tab key event
     def event(self, event):
+        """
+        Qt override.
+
+        This is needed to be able to intercept the Tab key press event.
+        """
         if event.type() == QEvent.KeyPress:
             if (event.key() == Qt.Key_Tab or event.key() == Qt.Key_Space):
                 text = self.text()
@@ -73,18 +79,20 @@ def event(self, event):
 
 
 class NumpyArrayTable(QTableWidget):
-    """ """
     def __init__(self, parent):
         QTableWidget.__init__(self, parent)
         self._parent = parent
         self.setRowCount(2)
         self.setColumnCount(2)
         self.reset_headers()
+
         # signals
         self.cellChanged.connect(self.cell_changed)
 
     def keyPressEvent(self, event):
-        """ """
+        """
+        Qt override.
+        """
         if event.key() in [Qt.Key_Enter, Qt.Key_Return]:
             QTableWidget.keyPressEvent(self, event)
             # To avoid having to enter one final tab
@@ -95,10 +103,15 @@ def keyPressEvent(self, event):
             QTableWidget.keyPressEvent(self, event)
 
     def cell_changed(self, row, col):
-        """ """
-        value = self.item(row, col).text()
-        rows = self.rowCount()
-        cols = self.columnCount()
+        """
+        """
+        item = self.item(row, col)
+        value = None
+
+        if item:
+            rows = self.rowCount()
+            cols = self.columnCount()
+            value = item.text()
 
         if value:
             if row == rows - 1:
@@ -108,7 +121,9 @@ def cell_changed(self, row, col):
         self.reset_headers()
 
     def reset_headers(self):
-        """ """
+        """
+        Update the column and row numbering in the headers.
+        """
         rows = self.rowCount()
         cols = self.columnCount()
 
@@ -119,7 +134,9 @@ def reset_headers(self):
             self.setColumnWidth(c, 40)
 
     def text(self):
-        """ """
+        """
+        Return the entered array in a parseable form.
+        """
         text = []
         rows = self.rowCount()
         cols = self.columnCount()
@@ -129,8 +146,6 @@ def text(self):
             item = self.item(0, 0)
             if item is None:
                 return ''
-            elif item.text() == '':
-                return ''
 
         for r in range(rows - 1):
             for c in range(cols - 1):
@@ -140,8 +155,9 @@ def text(self):
                 else:
                     value = '0'
 
-                if value == '':
+                if not value.strip():
                     value = '0'
+
                 text.append(' ')
                 text.append(value)
             text.append(ROW_SEPARATOR)
@@ -150,16 +166,15 @@ def text(self):
 
 
 class NumpyArrayDialog(QDialog):
-    """ """
-    def __init__(self, parent, inline=True, offset=0):
-        QDialog.__init__(self, parent)
+    def __init__(self, parent=None, inline=True, offset=0, force_float=False):
+        QDialog.__init__(self, parent=parent)
         self._parent = parent
         self._text = None
         self._valid = None
         self._offset = offset
 
         # TODO: add this as an option in the General Preferences?
-        self._force_float = False
+        self._force_float = force_float
 
         self._help_inline = _("""
            <b>Numpy Array/Matrix Helper</b><br>
@@ -183,7 +198,7 @@ def __init__(self, parent, inline=True, offset=0):
            Use two tabs at the end of a row to move to the next row.
            """)
 
-        # widgets
+        # Widgets
         self._button_warning = QToolButton()
         self._button_help = HelperToolButton()
         self._button_help.setIcon(ima.icon('MessageBoxInformation'))
@@ -242,7 +257,9 @@ def __init__(self, parent, inline=True, offset=0):
         self._widget.setFocus()
 
     def keyPressEvent(self, event):
-        """Override Qt method"""
+        """
+        Qt override.
+        """
         QToolTip.hideText()
         ctrl = event.modifiers() & Qt.ControlModifier
 
@@ -256,12 +273,19 @@ def keyPressEvent(self, event):
             QDialog.keyPressEvent(self, event)
 
     def event(self, event):
+        """
+        Qt Override.
+
+        Usefull when in line edit mode.
+        """
         if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab:
             return False
         return QWidget.event(self, event)
 
     def process_text(self, array=True):
-        """ """
+        """
+        Construct the text based on the entered content in the widget.
+        """
         if array:
             prefix = 'np.array([['
         else:
@@ -319,7 +343,7 @@ def process_text(self, array=True):
             if nrows == 1:
                 prefix = prefix[:-1]
                 suffix = suffix.replace("]])", "])")
-            
+
             # Fix offset
             offset = self._offset
             braces = BRACES.replace(' ', '\n' + ' '*(offset + len(prefix) - 1))
@@ -333,7 +357,9 @@ def process_text(self, array=True):
         self.update_warning()
 
     def update_warning(self):
-        """ """
+        """
+        Updates the icon and tip based on the validity of the array content.
+        """
         widget = self._button_warning
         if not self.is_valid():
             tip = _('Array dimensions not valid')
@@ -344,32 +370,34 @@ def update_warning(self):
             self._button_warning.setToolTip('')
 
     def is_valid(self):
-        """ """
+        """
+        Return if the current array state is valid.
+        """
         return self._valid
 
     def text(self):
-        """ """
+        """
+        Return the parsed array/matrix text.
+        """
         return self._text
 
-    def mousePressEvent(self, event):
-        """ """
-        pass
+    @property
+    def array_widget(self):
+        """
+        Return the array builder widget.
+        """
+        return self._widget
 
 
-def test():
+def test():  # pragma: no cover
     from spyderlib.utils.qthelpers import qapplication
     app = qapplication()
-    app.setStyle('Plastique')
-
     dlg_table = NumpyArrayDialog(None, inline=False)
     dlg_inline = NumpyArrayDialog(None, inline=True)
-
-    if dlg_table.exec_():
-        print(dlg_table.text())
-
-    if dlg_inline.exec_():
-        print(dlg_inline.text())
+    dlg_table.show()
+    dlg_inline.show()
+    app.exec_()
 
 
-if __name__ == "__main__":
+if __name__ == "__main__":  # pragma: no cover
     test()


Test patch:
diff --git a/continuous_integration/appveyor/modules_test.bat b/continuous_integration/appveyor/modules_test.bat
--- a/continuous_integration/appveyor/modules_test.bat
+++ b/continuous_integration/appveyor/modules_test.bat
@@ -42,6 +42,9 @@ for /r "%SPYDERLIB%" %%f in (*.py) do (
     ) else if "%%f"=="%SPYDERLIB%\utils\introspection\module_completion.py" (
         echo --- NOT testing %%f ---
         echo.
+    ) else if "%%f"=="%SPYDERLIB%\utils\introspection\plugin_client.py" (
+            echo --- NOT testing %%f ---
+            echo.
     ) else if "%%f"=="%SPYDERLIB%\widgets\externalshell\systemshell.py" (
         echo --- NOT testing %%f ---
         echo.
diff --git a/spyderlib/widgets/tests/__init__.py b/spyderlib/widgets/tests/__init__.py
new file mode 100644
diff --git a/spyderlib/widgets/tests/test_array_builder.py b/spyderlib/widgets/tests/test_array_builder.py
new file mode 100644
--- /dev/null
+++ b/spyderlib/widgets/tests/test_array_builder.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2009- The Spyder Development Team
+# Licensed under the terms of the MIT License
+#
+
+"""
+Tests for the Numpy Matrix/Array Builder Widget.
+"""
+
+# Third party imports
+from qtpy.QtCore import Qt
+from pytestqt import qtbot
+import pytest
+
+# Local imports
+from spyderlib.widgets.arraybuilder import NumpyArrayDialog
+
+
+# --- Fixtures
+# -----------------------------------------------------------------------------
+@pytest.fixture
+def botinline(qtbot):
+    dialog = NumpyArrayDialog(inline=True)
+    qtbot.addWidget(dialog)
+    dialog.show()
+    return qtbot, dialog, dialog.array_widget
+
+
+@pytest.fixture
+def botinlinefloat(qtbot):
+    dialog = NumpyArrayDialog(inline=True, force_float=True)
+    qtbot.addWidget(dialog)
+    dialog.show()
+    return qtbot, dialog, dialog.array_widget
+
+
+@pytest.fixture
+def botarray(qtbot):
+    dialog = NumpyArrayDialog(inline=False)
+    qtbot.addWidget(dialog)
+    dialog.show()
+    return qtbot, dialog, dialog.array_widget
+
+
+# --- Tests
+# -----------------------------------------------------------------------------
+def test_array_inline_array(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '1 2 3  4 5 6')
+    qtbot.keyPress(widget, Qt.Key_Return)
+    value = dialog.text()
+    assert value == 'np.array([[1, 2, 3],\n          [4, 5, 6]])'
+
+def test_array_inline_matrix(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '4 5 6  7 8 9')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([[4, 5, 6],\n           [7, 8, 9]])'
+
+def test_array_inline_array_invalid(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '1 2  3 4  5 6 7')
+    qtbot.keyPress(widget, Qt.Key_Return)
+    dialog.update_warning()
+    assert not dialog.is_valid()
+
+def test_array_inline_1d_array(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '4 5 6')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([4, 5, 6])'
+
+def test_array_inline_nan_array(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '4 nan 6 8 9')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([4, np.nan, 6, 8, 9])'
+
+def test_array_inline_force_float_array(botinlinefloat):
+    qtbot, dialog, widget = botinlinefloat
+    qtbot.keyClicks(widget, '4 5 6 8 9')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([4.0, 5.0, 6.0, 8.0, 9.0])'
+
+def test_array_inline_force_float_error_array(botinlinefloat):
+    qtbot, dialog, widget = botinlinefloat
+    qtbot.keyClicks(widget, '4 5 6 a 9')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([4.0, 5.0, 6.0, a, 9.0])'
+
+def test_array_table_array(botarray):
+    qtbot, dialog, widget = botarray
+    qtbot.keyClick(widget, Qt.Key_1)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_2)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Backtab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_3)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_4)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_5)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_6)
+    qtbot.keyClick(widget, Qt.Key_Tab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_Return, modifier=Qt.NoModifier)
+    value = dialog.text()
+    assert value == 'np.array([[1, 2, 3],\n          [4, 5, 6]])'
+
+def test_array_table_matrix(botarray):  # analysis:ignore
+    qtbot, dialog, widget = botarray
+    qtbot.keyClick(widget, Qt.Key_1)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_2)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Backtab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_3)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_4)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_5)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_6)
+    qtbot.keyClick(widget, Qt.Key_Tab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([[1, 2, 3],\n           [4, 5, 6]])'
+
+def test_array_table_array_empty_items(botarray):  # analysis:ignore
+    qtbot, dialog, widget = botarray
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_2)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Backtab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_3)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_5)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_6)
+    qtbot.keyClick(widget, Qt.Key_Tab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_Return, modifier=Qt.NoModifier)
+    value = dialog.text()
+    assert value == 'np.array([[0, 2, 3],\n          [0, 5, 6]])'
+
+def test_array_table_array_spaces_in_item(botarray):  # analysis:ignore
+    qtbot, dialog, widget = botarray
+    qtbot.keyClicks(widget, '   ')
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_2)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Backtab)
+    qtbot.keyClick(widget, Qt.Key_3)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_5)
+    qtbot.keyClick(widget, Qt.Key_Tab)
+    qtbot.keyClick(widget, Qt.Key_6)
+    qtbot.keyClick(widget, Qt.Key_Tab)  # Hack: in the tests the selected cell is wrong
+    qtbot.keyClick(widget, Qt.Key_Return, modifier=Qt.NoModifier)
+    value = dialog.text()
+    assert value == 'np.array([[0, 2, 3],\n          [0, 5, 6]])'
+
+def test_array_table_matrix_empty(botarray):  # analysis:ignore
+    qtbot, dialog, widget = botarray
+    qtbot.keyClick(widget, Qt.Key_Return, modifier=Qt.NoModifier)
+    value = dialog.text()
+    assert value == ''