Repository: spyder-ide/spyder

Issue:
Add inf support to array builder!
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [ ] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [ ] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [ ] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ ] Tried basic troubleshooting (if a bug/error)
    * [ ] Restarted Spyder
    * [ ] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description



### What steps reproduce the problem?

1. 
2. 
3. 

### What is the expected output? What do you see instead?



### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback

PASTE TRACEBACK HERE

```

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 
* Python version: 
* Qt version: 
* PyQt version: 
* Operating System name/version: 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
PASTE DEPENDENCIES HERE
```



Discussion:

Resolving patch:
diff --git a/spyder/widgets/arraybuilder.py b/spyder/widgets/arraybuilder.py
--- a/spyder/widgets/arraybuilder.py
+++ b/spyder/widgets/arraybuilder.py
@@ -4,14 +4,14 @@
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
 
-"""
-Numpy Matrix/Array Builder Widget.
-"""
+"""Array Builder Widget."""
 
 # TODO:
-# -Set font based on caller? editor console? and adjust size of widget
-# -Fix positioning
-# -Use the same font as editor/console?
+# - Set font based on caller? editor console? and adjust size of widget
+# - Fix positioning
+# - Use the same font as editor/console?
+# - Generalize separators
+# - Generalize API for registering new array builders
 
 # Standard library imports
 from __future__ import division
@@ -29,36 +29,60 @@
 from spyder.utils import icon_manager as ima
 from spyder.widgets.helperwidgets import HelperToolButton
 
-
 # Constants
 SHORTCUT_TABLE = "Ctrl+M"
 SHORTCUT_INLINE = "Ctrl+Alt+M"
-ELEMENT_SEPARATOR = ', '
-ROW_SEPARATOR = ';'
-BRACES = '], ['
-NAN_VALUES = ['nan', 'NAN', 'NaN', 'Na', 'NA', 'na']
 
 
-class NumpyArrayInline(QLineEdit):
-    def __init__(self, parent):
-        QLineEdit.__init__(self, parent)
+class ArrayBuilderType:
+    LANGUAGE = None
+    ELEMENT_SEPARATOR = None
+    ROW_SEPARATOR = None
+    BRACES = None
+    EXTRA_VALUES = None
+    ARRAY_PREFIX = None
+    MATRIX_PREFIX = None
+
+    def check_values(self):
+        pass
+
+
+class ArrayBuilderPython(ArrayBuilderType):
+    ELEMENT_SEPARATOR = ', '
+    ROW_SEPARATOR = ';'
+    BRACES = '], ['
+    EXTRA_VALUES = {
+        'np.nan': ['nan', 'NAN', 'NaN', 'Na', 'NA', 'na'],
+        'np.inf': ['inf', 'INF'],
+    }
+    ARRAY_PREFIX = 'np.array([['
+    MATRIX_PREFIX = 'np.matrix([['
+
+
+_REGISTERED_ARRAY_BUILDERS = {
+    'python': ArrayBuilderPython,
+}
+
+
+class ArrayInline(QLineEdit):
+    def __init__(self, parent, options=None):
+        super(ArrayInline, self).__init__(parent)
         self._parent = parent
+        self._options = options
 
     def keyPressEvent(self, event):
-        """
-        Qt override.
-        """
+        """Override Qt method."""
         if event.key() in [Qt.Key_Enter, Qt.Key_Return]:
             self._parent.process_text()
             if self._parent.is_valid():
                 self._parent.keyPressEvent(event)
         else:
-            QLineEdit.keyPressEvent(self, event)
+            super(ArrayInline, self).keyPressEvent(event)
 
-    # to catch the Tab key event
+    # To catch the Tab key event
     def event(self, event):
         """
-        Qt override.
+        Override Qt method.
 
         This is needed to be able to intercept the Tab key press event.
         """
@@ -66,23 +90,27 @@ def event(self, event):
             if (event.key() == Qt.Key_Tab or event.key() == Qt.Key_Space):
                 text = self.text()
                 cursor = self.cursorPosition()
-                # fix to include in "undo/redo" history
+
+                # Fix to include in "undo/redo" history
                 if cursor != 0 and text[cursor-1] == ' ':
-                    text = text[:cursor-1] + ROW_SEPARATOR + ' ' +\
-                        text[cursor:]
+                    text = (text[:cursor-1] + self._options.ROW_SEPARATOR
+                            + ' ' + text[cursor:])
                 else:
                     text = text[:cursor] + ' ' + text[cursor:]
                 self.setCursorPosition(cursor)
                 self.setText(text)
                 self.setCursorPosition(cursor + 1)
+
                 return False
-        return QWidget.event(self, event)
+
+        return super(ArrayInline, self).event(event)
 
 
-class NumpyArrayTable(QTableWidget):
-    def __init__(self, parent):
-        QTableWidget.__init__(self, parent)
+class ArrayTable(QTableWidget):
+    def __init__(self, parent, options=None):
+        super(ArrayTable, self).__init__(parent)
         self._parent = parent
+        self._options = options
         self.setRowCount(2)
         self.setColumnCount(2)
         self.reset_headers()
@@ -91,21 +119,15 @@ def __init__(self, parent):
         self.cellChanged.connect(self.cell_changed)
 
     def keyPressEvent(self, event):
-        """
-        Qt override.
-        """
+        """Override Qt method."""
+        super(ArrayTable, self).keyPressEvent(event)
         if event.key() in [Qt.Key_Enter, Qt.Key_Return]:
-            QTableWidget.keyPressEvent(self, event)
             # To avoid having to enter one final tab
             self.setDisabled(True)
             self.setDisabled(False)
             self._parent.keyPressEvent(event)
-        else:
-            QTableWidget.keyPressEvent(self, event)
 
     def cell_changed(self, row, col):
-        """
-        """
         item = self.item(row, col)
         value = None
 
@@ -122,9 +144,7 @@ def cell_changed(self, row, col):
         self.reset_headers()
 
     def reset_headers(self):
-        """
-        Update the column and row numbering in the headers.
-        """
+        """Update the column and row numbering in the headers."""
         rows = self.rowCount()
         cols = self.columnCount()
 
@@ -135,9 +155,7 @@ def reset_headers(self):
             self.setColumnWidth(c, 40)
 
     def text(self):
-        """
-        Return the entered array in a parseable form.
-        """
+        """Return the entered array in a parseable form."""
         text = []
         rows = self.rowCount()
         cols = self.columnCount()
@@ -161,14 +179,17 @@ def text(self):
 
                 text.append(' ')
                 text.append(value)
-            text.append(ROW_SEPARATOR)
+            text.append(self._options.ROW_SEPARATOR)
 
-        return ''.join(text[:-1])  # to remove the final uneeded ;
+        return ''.join(text[:-1])  # Remove the final uneeded `;`
 
 
-class NumpyArrayDialog(QDialog):
-    def __init__(self, parent=None, inline=True, offset=0, force_float=False):
-        QDialog.__init__(self, parent=parent)
+class ArrayBuilderDialog(QDialog):
+    def __init__(self, parent=None, inline=True, offset=0, force_float=False,
+                 language='python'):
+        super(ArrayBuilderDialog, self).__init__(parent=parent)
+        self._language = language
+        self._options = _REGISTERED_ARRAY_BUILDERS.get('python', None)
         self._parent = parent
         self._text = None
         self._valid = None
@@ -228,11 +249,11 @@ def __init__(self, parent=None, inline=True, offset=0, force_float=False):
 
         if inline:
             self._button_help.setToolTip(self._help_inline)
-            self._text = NumpyArrayInline(self)
+            self._text = ArrayInline(self, options=self._options)
             self._widget = self._text
         else:
             self._button_help.setToolTip(self._help_table)
-            self._table = NumpyArrayTable(self)
+            self._table = ArrayTable(self, options=self._options)
             self._widget = self._table
 
         style = """
@@ -269,9 +290,7 @@ def __init__(self, parent=None, inline=True, offset=0, force_float=False):
         self._widget.setFocus()
 
     def keyPressEvent(self, event):
-        """
-        Qt override.
-        """
+        """Override Qt method."""
         QToolTip.hideText()
         ctrl = event.modifiers() & Qt.ControlModifier
 
@@ -282,57 +301,59 @@ def keyPressEvent(self, event):
                 self.process_text(array=True)
             self.accept()
         else:
-            QDialog.keyPressEvent(self, event)
+            super(ArrayBuilderDialog, self).keyPressEvent(event)
 
     def event(self, event):
         """
-        Qt Override.
+        Override Qt method.
 
         Usefull when in line edit mode.
         """
         if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab:
             return False
-        return QWidget.event(self, event)
+
+        return super(ArrayBuilderDialog, self).event(event)
 
     def process_text(self, array=True):
         """
         Construct the text based on the entered content in the widget.
         """
         if array:
-            prefix = 'np.array([['
+            prefix = self._options.ARRAY_PREFIX
         else:
-            prefix = 'np.matrix([['
+            prefix = self._options.MATRIX_PREFIX
 
         suffix = ']])'
         values = self._widget.text().strip()
 
         if values != '':
             # cleans repeated spaces
-            exp = r'(\s*)' + ROW_SEPARATOR + r'(\s*)'
-            values = re.sub(exp, ROW_SEPARATOR, values)
+            exp = r'(\s*)' + self._options.ROW_SEPARATOR + r'(\s*)'
+            values = re.sub(exp, self._options.ROW_SEPARATOR, values)
             values = re.sub(r"\s+", " ", values)
             values = re.sub(r"]$", "", values)
             values = re.sub(r"^\[", "", values)
-            values = re.sub(ROW_SEPARATOR + r'*$', '', values)
+            values = re.sub(self._options.ROW_SEPARATOR + r'*$', '', values)
 
             # replaces spaces by commas
-            values = values.replace(' ',  ELEMENT_SEPARATOR)
+            values = values.replace(' ',  self._options.ELEMENT_SEPARATOR)
 
             # iterate to find number of rows and columns
             new_values = []
-            rows = values.split(ROW_SEPARATOR)
+            rows = values.split(self._options.ROW_SEPARATOR)
             nrows = len(rows)
             ncols = []
             for row in rows:
                 new_row = []
-                elements = row.split(ELEMENT_SEPARATOR)
+                elements = row.split(self._options.ELEMENT_SEPARATOR)
                 ncols.append(len(elements))
                 for e in elements:
                     num = e
 
                     # replaces not defined values
-                    if num in NAN_VALUES:
-                        num = 'np.nan'
+                    for key, values in self._options.EXTRA_VALUES.items():
+                        if num in values:
+                            num = key
 
                     # Convert numbers to floating point
                     if self._force_float:
@@ -341,8 +362,9 @@ def process_text(self, array=True):
                         except:
                             pass
                     new_row.append(num)
-                new_values.append(ELEMENT_SEPARATOR.join(new_row))
-            new_values = ROW_SEPARATOR.join(new_values)
+                new_values.append(
+                    self._options.ELEMENT_SEPARATOR.join(new_row))
+            new_values = self._options.ROW_SEPARATOR.join(new_values)
             values = new_values
 
             # Check validity
@@ -358,14 +380,16 @@ def process_text(self, array=True):
 
             # Fix offset
             offset = self._offset
-            braces = BRACES.replace(' ', '\n' + ' '*(offset + len(prefix) - 1))
-
-            values = values.replace(ROW_SEPARATOR,  braces)
+            braces = self._options.BRACES.replace(
+                ' ',
+                '\n' + ' '*(offset + len(prefix) - 1))
+            values = values.replace(self._options.ROW_SEPARATOR,  braces)
             text = "{0}{1}{2}".format(prefix, values, suffix)
 
             self._text = text
         else:
             self._text = ''
+
         self.update_warning()
 
     def update_warning(self):
@@ -382,30 +406,24 @@ def update_warning(self):
             self._button_warning.setToolTip('')
 
     def is_valid(self):
-        """
-        Return if the current array state is valid.
-        """
+        """Return if the current array state is valid."""
         return self._valid
 
     def text(self):
-        """
-        Return the parsed array/matrix text.
-        """
+        """Return the parsed array/matrix text."""
         return self._text
 
     @property
     def array_widget(self):
-        """
-        Return the array builder widget.
-        """
+        """Return the array builder widget."""
         return self._widget
 
 
 def test():  # pragma: no cover
     from spyder.utils.qthelpers import qapplication
     app = qapplication()
-    dlg_table = NumpyArrayDialog(None, inline=False)
-    dlg_inline = NumpyArrayDialog(None, inline=True)
+    dlg_table = ArrayBuilderDialog(None, inline=False)
+    dlg_inline = ArrayBuilderDialog(None, inline=True)
     dlg_table.show()
     dlg_inline.show()
     app.exec_()
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -37,7 +37,7 @@
 from spyder.py3compat import is_text_string, to_text_string
 from spyder.utils import encoding, sourcecode, programs
 from spyder.utils.misc import get_error_match
-from spyder.widgets.arraybuilder import NumpyArrayDialog
+from spyder.widgets.arraybuilder import ArrayBuilderDialog
 
 QT55_VERSION = programs.check_version(QT_VERSION, "5.5", ">=")
 
@@ -1109,20 +1109,20 @@ def get_match_number(self, pattern, case=False, regexp=False, word=False):
                                                word=word)
         return match_number
 
-    # --- Numpy matrix/array helper / See 'spyder/widgets/arraybuilder.py'
+    # --- Array builder helper / See 'spyder/widgets/arraybuilder.py'
     def enter_array_inline(self):
-        """ """
+        """Enter array builder inline mode."""
         self._enter_array(True)
 
     def enter_array_table(self):
-        """ """
+        """Enter array builder table mode."""
         self._enter_array(False)
 
     def _enter_array(self, inline):
-        """ """
+        """Enter array builder mode."""
         offset = self.get_position('cursor') - self.get_position('sol')
         rect = self.cursorRect()
-        dlg = NumpyArrayDialog(self, inline, offset)
+        dlg = ArrayBuilderDialog(self, inline, offset)
 
         # TODO: adapt to font size
         x = rect.left()


Test patch:
diff --git a/spyder/widgets/tests/test_array_builder.py b/spyder/widgets/tests/test_array_builder.py
--- a/spyder/widgets/tests/test_array_builder.py
+++ b/spyder/widgets/tests/test_array_builder.py
@@ -4,9 +4,7 @@
 # Licensed under the terms of the MIT License
 #
 
-"""
-Tests for the Numpy Matrix/Array Builder Widget.
-"""
+"""Tests for the Array Builder Widget."""
 
 # Standard library imports
 import sys
@@ -16,14 +14,14 @@
 import pytest
 
 # Local imports
-from spyder.widgets.arraybuilder import NumpyArrayDialog
+from spyder.widgets.arraybuilder import ArrayBuilderDialog
 
 
 # --- Fixtures
 # -----------------------------------------------------------------------------
 @pytest.fixture
 def botinline(qtbot):
-    dialog = NumpyArrayDialog(inline=True)
+    dialog = ArrayBuilderDialog(inline=True)
     qtbot.addWidget(dialog)
     dialog.show()
     return qtbot, dialog, dialog.array_widget
@@ -31,7 +29,7 @@ def botinline(qtbot):
 
 @pytest.fixture
 def botinlinefloat(qtbot):
-    dialog = NumpyArrayDialog(inline=True, force_float=True)
+    dialog = ArrayBuilderDialog(inline=True, force_float=True)
     qtbot.addWidget(dialog)
     dialog.show()
     return qtbot, dialog, dialog.array_widget
@@ -39,7 +37,7 @@ def botinlinefloat(qtbot):
 
 @pytest.fixture
 def botarray(qtbot):
-    dialog = NumpyArrayDialog(inline=False)
+    dialog = ArrayBuilderDialog(inline=False)
     qtbot.addWidget(dialog)
     dialog.show()
     return qtbot, dialog, dialog.array_widget
@@ -54,6 +52,7 @@ def test_array_inline_array(botinline):
     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')
@@ -61,6 +60,7 @@ def test_array_inline_matrix(botinline):
     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')
@@ -68,6 +68,7 @@ def test_array_inline_array_invalid(botinline):
     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')
@@ -75,6 +76,7 @@ def test_array_inline_1d_array(botinline):
     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')
@@ -82,6 +84,15 @@ def test_array_inline_nan_array(botinline):
     value = dialog.text()
     assert value == 'np.matrix([4, np.nan, 6, 8, 9])'
 
+
+def test_array_inline_inf_array(botinline):
+    qtbot, dialog, widget = botinline
+    qtbot.keyClicks(widget, '4 inf 6 8 9')
+    qtbot.keyPress(widget, Qt.Key_Return, modifier=Qt.ControlModifier)
+    value = dialog.text()
+    assert value == 'np.matrix([4, np.inf, 6, 8, 9])'
+
+
 def test_array_inline_force_float_array(botinlinefloat):
     qtbot, dialog, widget = botinlinefloat
     qtbot.keyClicks(widget, '4 5 6 8 9')
@@ -89,6 +100,7 @@ def test_array_inline_force_float_array(botinlinefloat):
     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')
@@ -96,6 +108,7 @@ def test_array_inline_force_float_error_array(botinlinefloat):
     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)
@@ -115,6 +128,7 @@ def test_array_table_array(botarray):
     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)
@@ -134,6 +148,7 @@ def test_array_table_matrix(botarray):  # analysis:ignore
     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)
@@ -151,6 +166,7 @@ def test_array_table_array_empty_items(botarray):  # analysis:ignore
     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, '   ')
@@ -169,6 +185,7 @@ def test_array_table_array_spaces_in_item(botarray):  # analysis:ignore
     value = dialog.text()
     assert value == 'np.array([[0, 2, 3],\n          [0, 5, 6]])'
 
+
 @pytest.mark.skipif(sys.platform == 'darwin', reason="It fails on macOS")
 def test_array_table_matrix_empty(botarray):  # analysis:ignore
     qtbot, dialog, widget = botarray