Repository: spyder-ide/spyder

Issue:
Variable type and size are out of order
## Problem Description

Variable explorer problem, variable type and size are out of order

## Test Code
```python
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
class IndDistIdent(object):
    def __init__(self):
        self.algName = {'algName':'algName'}
        self.keysZhCN = {'keysZhCN':
                             {
                                 'returnName':'returnName'
                             }
                        }
    def main(self,data):
        desSta = self.__desSta(data)
        ret = {**desSta,**self.algName,**self.keysZhCN}
        print(type(ret.get('algName')),len(ret.get('algName')))
        print(type(ret.get('DesSta')),len(ret.get('DesSta')))

        return ret

    def __desSta(self,data):
        n = len(data)
        mean = data.mean()
        sigma = data.std()
        med = data.median()
        minV = data.min()
        maxV = data.max()
        skew = data.skew()
        kurt = data.kurt()
        values = [n,mean,sigma,med,minV,maxV,skew,kurt]
        index = ['N','Mean','Sigma','Median','Min','Max','Skew','Kurt']
        ret = pd.Series(values,index=index,name='DesSta')
        return {'DesSta':ret}

if __name__ == '__main__':
    data = pd.Series(np.random.normal(loc=0,scale=1,size=100))
    idi = IndDistIdent()
    ret = idi.main(data)
```

### What steps reproduce the problem?
 
![spyder4 1 5变量查看bug](https://user-images.githubusercontent.com/1759365/92431101-2f269680-f1c9-11ea-8d01-dd5895297a5a.gif)

The type and size of the algName and desSta variables in ret are out of order

## 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: 4.1.5
* Python version: 3.7.6
* Qt version: 5.9.6
* PyQt version: 5.9.2
* Operating System name/version: win10


Discussion:
Hey @leavor, thanks for reporting. This error was probably introduced in PR #13558.

@skjerns, could you take a look at this one? Thanks! 

Resolving patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py
--- a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py
+++ b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py
@@ -252,16 +252,20 @@ def is_editable_type(value):
 #==============================================================================
 # Sorting
 #==============================================================================
-def sort_against(list1, list2, reverse=False):
+def sort_against(list1, list2, reverse=False, sort_key=None):
     """
     Arrange items of list1 in the same order as sorted(list2).
 
     In other words, apply to list1 the permutation which takes list2 
     to sorted(list2, reverse).
     """
+    if sort_key is None:
+        key = lambda x: x[0]
+    else:
+        key = lambda x: sort_key(x[0])
     try:
         return [item for _, item in 
-                sorted(zip(list2, list1), key=lambda x: x[0], reverse=reverse)]
+                sorted(zip(list2, list1), key=key, reverse=reverse)]
     except:
         return list1
 
diff --git a/spyder/plugins/variableexplorer/widgets/collectionseditor.py b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
--- a/spyder/plugins/variableexplorer/widgets/collectionseditor.py
+++ b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
@@ -72,7 +72,8 @@ def natsort(s):
     Natural sorting, e.g. test3 comes before test100.
     Taken from https://stackoverflow.com/a/16090640/3110740
     """
-    # if not isinstance(s, (str, bytes)): return s
+    if not isinstance(s, (str, bytes)):
+        return s
     x = [int(t) if t.isdigit() else t.lower() for t in re.split('([0-9]+)', s)]
     return x
 
@@ -261,33 +262,37 @@ def all_string(listlike):
         sort_key = natsort if all_string(self.keys) else None
 
         if column == 0:
-            self.sizes = sort_against(self.sizes, self.keys, reverse)
-            self.types = sort_against(self.types, self.keys, reverse)
+            self.sizes = sort_against(self.sizes, self.keys,
+                                      reverse=reverse,
+                                      sort_key=natsort)
+            self.types = sort_against(self.types, self.keys,
+                                      reverse=reverse,
+                                      sort_key=natsort)
             try:
                 self.keys.sort(reverse=reverse, key=sort_key)
             except:
                 pass
         elif column == 1:
-            self.keys[:self.rows_loaded] = sort_against(self.keys, self.types,
-                                                        reverse)
-            self.sizes = sort_against(self.sizes, self.types, reverse)
+            self.keys[:self.rows_loaded] = sort_against(self.keys,
+                                                        self.types,
+                                                        reverse=reverse)
+            self.sizes = sort_against(self.sizes, self.types, reverse=reverse)
             try:
-                self.types.sort(reverse=reverse, key=sort_key)
+                self.types.sort(reverse=reverse)
             except:
                 pass
         elif column == 2:
-            self.keys[:self.rows_loaded] = sort_against(self.keys, self.sizes,
-                                                        reverse)
-            self.types = sort_against(self.types, self.sizes, reverse)
+            self.keys[:self.rows_loaded] = sort_against(self.keys, self.sizes)
+            self.types = sort_against(self.types, self.sizes, reverse=reverse)
             try:
-                self.sizes.sort(reverse=reverse, key=sort_key)
+                self.sizes.sort(reverse=reverse)
             except:
                 pass
         elif column in [3, 4]:
             values = [self._data[key] for key in self.keys]
-            self.keys = sort_against(self.keys, values, reverse)
-            self.sizes = sort_against(self.sizes, values, reverse)
-            self.types = sort_against(self.types, values, reverse)
+            self.keys = sort_against(self.keys, values, reverse=reverse)
+            self.sizes = sort_against(self.sizes, values, reverse=reverse)
+            self.types = sort_against(self.types, values, reverse=reverse)
         self.beginResetModel()
         self.endResetModel()
 
@@ -1694,7 +1699,11 @@ def filterAcceptsRow(self, row_num, parent):
             return True
 
     def lessThan(self, left, right):
-        """Implements ordering in a natural way, as a human would sort."""
+        """
+        Implements ordering in a natural way, as a human would sort.
+        This functions enables sorting of the main variable editor table,
+        which does not rely on 'self.sort()'.
+        """
         leftData = self.sourceModel().data(left)
         rightData = self.sourceModel().data(right)
         if isinstance(leftData, str) and isinstance(rightData, str):


Test patch:
diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py b/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
--- a/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
+++ b/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
@@ -38,6 +38,7 @@
 from spyder.plugins.variableexplorer.widgets.tests.test_dataframeeditor import \
     generate_pandas_indexes
 from spyder.py3compat import PY2, to_text_string
+from spyder_kernels.utils.nsview import get_size
 
 
 # =============================================================================
@@ -845,5 +846,58 @@ def test_dicts_natural_sorting(qtbot):
         'GUI sorting fail'
 
 
+def test_dicts_natural_sorting_mixed_types(qtbot):
+    """
+    Test that natural sorting actually does what it should do.
+    testing for issue 13733, as mixed types were sorted incorrectly.
+
+    Sorting for other columns will be tested as well.
+    """
+    import pandas as pd
+    dictionary = {'DSeries': pd.Series(dtype=int), 'aStr': 'algName',
+                  'kDict': {2: 'asd', 3: 2}}
+
+    # put this here variable, as it might change later to reflect string length
+    str_size = get_size(dictionary['aStr'])
+
+    editor = CollectionsEditor()
+    editor.setup(dictionary)
+    cm = editor.widget.editor.source_model
+    cm.sort(0)
+    keys = cm.keys
+    types = cm.types
+    sizes = cm.sizes
+
+    assert keys == ['aStr', 'DSeries', 'kDict']
+    assert types == ['str', 'Series', 'dict']
+    assert sizes == [1, (0,), 2]
+
+    assert data_table(cm, 3, 3) == [['aStr', 'DSeries', 'kDict'],
+                                    ['str', 'Series', 'dict'],
+                                    [1, '(0,)', 2]]
+
+    # insert an item and check that it is still sorted correctly
+    editor.widget.editor.new_value('List', [1, 2, 3])
+    assert data_table(cm, 4, 3) == [['aStr', 'DSeries', 'kDict', 'List'],
+                                    ['str', 'Series', 'dict', 'list'],
+                                    [1, '(0,)', 2, 3]]
+    cm.sort(0)
+    assert data_table(cm, 4, 3) == [['aStr', 'DSeries', 'kDict', 'List'],
+                                    ['str', 'Series', 'dict', 'list'],
+                                    [1, '(0,)', 2, 3]]
+
+    # now sort for types
+    cm.sort(1)
+    assert data_table(cm, 4, 3) == [['DSeries', 'kDict', 'List', 'aStr'],
+                                    ['Series', 'dict', 'list', 'str'],
+                                    ['(0,)', 2, 3, 1]]
+
+    # now sort for sizes
+    cm.sort(2)
+    assert data_table(cm, 4, 3) == [['DSeries', 'kDict', 'List', 'aStr'],
+                                    ['Series', 'dict', 'list', 'str'],
+                                    ['(0,)', 2, 3, str_size]]
+
+
 if __name__ == "__main__":
     pytest.main()