From fc7ee4928eab76342a34e5cbd70b31f3b434f12e Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Thu, 24 Jan 2019 15:15:19 +0100
Subject: [PATCH 1/8] split test scripts

---
 larray_editor/tests/test_compare.py           | 26 +++++++
 .../{test_api_larray.py => test_data.py}      | 71 +++----------------
 larray_editor/tests/test_edit.py              | 25 +++++++
 3 files changed, 61 insertions(+), 61 deletions(-)
 create mode 100644 larray_editor/tests/test_compare.py
 rename larray_editor/tests/{test_api_larray.py => test_data.py} (69%)
 create mode 100644 larray_editor/tests/test_edit.py

diff --git a/larray_editor/tests/test_compare.py b/larray_editor/tests/test_compare.py
new file mode 100644
index 0000000..302c306
--- /dev/null
+++ b/larray_editor/tests/test_compare.py
@@ -0,0 +1,26 @@
+from __future__ import absolute_import, division, print_function
+
+"""Array editor test"""
+
+import logging
+from larray import Session, where
+
+from larray_editor.api import *
+from larray_editor.utils import logger
+from larray_editor.tests.test_data import *
+
+
+logger.setLevel(logging.DEBUG)
+
+
+compare(arr3, arr3 + 1.0)
+compare(np.random.normal(0, 1, size=(10, 2)), np.random.normal(0, 1, size=(10, 2)))
+compare(Session(arr4=arr4, arr3=arr3, data=data2),
+        Session(arr4=arr4 + 1.0, arr3=arr3 * 2.0, data=data2 * 1.05))
+# compare(Session(arr2=arr2, arr3=arr3),
+#         Session(arr2=arr2 + 1.0, arr3=arr3 * 2.0))
+
+arr1 = ndtest((3, 3))
+arr2 = 2 * arr1
+arr3 = where(arr1 % 2 == 0, arr1, -arr1)
+compare(arr1, arr2, arr3)
\ No newline at end of file
diff --git a/larray_editor/tests/test_api_larray.py b/larray_editor/tests/test_data.py
similarity index 69%
rename from larray_editor/tests/test_api_larray.py
rename to larray_editor/tests/test_data.py
index 1c3f0a2..084f850 100644
--- a/larray_editor/tests/test_api_larray.py
+++ b/larray_editor/tests/test_data.py
@@ -1,18 +1,9 @@
 from __future__ import absolute_import, division, print_function
 
-"""Array editor test"""
-
-import logging
-from larray_editor.api import *
-from larray_editor.utils import logger
-
 import numpy as np
-from larray import (Session, Axis, LArray, ndtest, zeros, from_lists, union,
-                    sin, cos, radians, maximum, sqrt, where)
+from larray import Axis, LArray, Session, ndtest, zeros, from_lists, union, sin, cos, radians, maximum, sqrt
 
 
-logger.setLevel(logging.DEBUG)
-
 lipro = Axis(['P%02d' % i for i in range(1, 16)], 'lipro')
 age = Axis('age=0..115')
 sex = Axis('sex=M,F')
@@ -41,6 +32,10 @@
 # arr2 = ndrange([100, 100, 100, 100, 5])
 # arr2 = arr2['F', 'A11', 1]
 
+ses2 = Session()
+ses2['data'] = data2
+ses2['arr'] = arr2
+
 # view(arr2[0, 'A11', 'F', 'P01'])
 # view(arr1)
 # view(arr2[0, 'A11'])
@@ -69,6 +64,10 @@
 # data4 = np.random.normal(0, 1, size=(2, 15))
 # arr4 = LArray(data4, axes=(sex, lipro))
 
+ses3 = Session()
+ses3['data'] = data3
+ses3['arr'] = arr3
+
 # arr4 = arr3.copy()
 # arr4['F'] /= 2
 arr4 = arr3.min(sex)
@@ -112,54 +111,4 @@ def make_demo(width=20, ball_radius=5, path_radius=5, steps=30):
 
 # test autoresizing
 long_labels = zeros('a=a_long_label,another_long_label; b=this_is_a_label,this_is_another_one')
-long_axes_names = zeros('first_axis=a0,a1; second_axis=b0,b1')
-
-# compare(arr3, arr4, arr5, arr6)
-
-# view(stack((arr3, arr4), Axis('arrays=arr3,arr4')))
-# ses = Session(arr2=arr2, arr3=arr3, arr4=arr4, arr5=arr5, arr6=arr6, arr7=arr7, long_labels=long_labels,
-#                  long_axes_names=long_axes_names, data2=data2, data3=data3)
-
-# from larray.tests.common import abspath
-# file = abspath('test_session.xlsx')
-# ses.save(file)
-
-# import cProfile as profile
-# profile.runctx('edit(Session(arr2=arr2))', vars(), {},
-#                'c:\\tmp\\edit.profile')
-edit()
-# edit(ses)
-# edit(file)
-# edit('fake_path')
-# edit(REOPEN_LAST_FILE)
-
-edit(arr2)
-
-compare(arr3, arr3 + 1.0)
-compare(np.random.normal(0, 1, size=(10, 2)), np.random.normal(0, 1, size=(10, 2)))
-compare(Session(arr4=arr4, arr3=arr3, data=data2),
-        Session(arr4=arr4 + 1.0, arr3=arr3 * 2.0, data=data2 * 1.05))
-# compare(Session(arr2=arr2, arr3=arr3),
-#         Session(arr2=arr2 + 1.0, arr3=arr3 * 2.0))
-
-# s = local_arrays()
-# view(s)
-# print('HDF')
-# s.save('x.h5')
-# print('\nEXCEL')
-# s.save('x.xlsx')
-# print('\nCSV')
-# s.save('x_csv')
-# print('\n open HDF')
-# edit('x.h5')
-# print('\n open EXCEL')
-# edit('x.xlsx')
-# print('\n open CSV')
-# edit('x_csv')
-
-arr1 = ndtest((3, 3))
-arr2 = 2 * arr1
-arr3 = where(arr1 % 2 == 0, arr1, -arr1)
-compare(arr1, arr2, arr3)
-
-
+long_axes_names = zeros('first_axis=a0,a1; second_axis=b0,b1')
\ No newline at end of file
diff --git a/larray_editor/tests/test_edit.py b/larray_editor/tests/test_edit.py
new file mode 100644
index 0000000..bb9bcf7
--- /dev/null
+++ b/larray_editor/tests/test_edit.py
@@ -0,0 +1,25 @@
+from __future__ import absolute_import, division, print_function
+
+"""Array editor test"""
+
+import logging
+
+from larray_editor.api import *
+from larray_editor.utils import logger
+from larray_editor.tests.test_data import *
+
+
+logger.setLevel(logging.DEBUG)
+
+
+# import cProfile as profile
+# profile.runctx('edit(Session(arr2=arr2))', vars(), {},
+#                'c:\\tmp\\edit.profile')
+
+edit()
+# edit(ses)
+# edit(file)
+# edit('fake_path')
+# edit(REOPEN_LAST_FILE)
+
+edit(arr2)

From 580f65c50d194c19344e566b20adf816e569fa5c Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Mon, 28 Jan 2019 09:18:55 +0100
Subject: [PATCH 2/8] made edit() function to accept arrays and sessions

---
 larray_editor/api.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/larray_editor/api.py b/larray_editor/api.py
index 8674668..a4b0a40 100644
--- a/larray_editor/api.py
+++ b/larray_editor/api.py
@@ -117,13 +117,13 @@ def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth
         obj.update([(k, global_vars[k]) for k in sorted(global_vars.keys())])
         obj.update([(k, local_vars[k]) for k in sorted(local_vars.keys())])
 
-    if not isinstance(obj, la.Session) and hasattr(obj, 'keys'):
-        obj = la.Session(obj)
+    if hasattr(obj, 'keys'):
+        obj = OrderedDict(obj)
 
     if not title and obj is not REOPEN_LAST_FILE:
         title = get_title(obj, depth=depth + 1)
 
-    if obj is REOPEN_LAST_FILE or isinstance(obj, (str, la.Session)):
+    if obj is REOPEN_LAST_FILE or isinstance(obj, (str, OrderedDict)):
         dlg = MappingEditor(parent)
         assert minvalue is None and maxvalue is None
         setup_ok = dlg.setup_and_check(obj, title=title, readonly=readonly)

From 44cfd9ef082ecfcd050d0487f6e1a3439dfd2a6e Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Thu, 24 Jan 2019 14:56:04 +0100
Subject: [PATCH 3/8] - implemented MapItem class - implemented MapItems class
 - replaced the use of a QListWidget instance by a QTreeWidget instance

---
 larray_editor/api.py    |  11 +-
 larray_editor/editor.py | 524 +++++++++++++++++++++++++++++-----------
 2 files changed, 395 insertions(+), 140 deletions(-)

diff --git a/larray_editor/api.py b/larray_editor/api.py
index a4b0a40..5543021 100644
--- a/larray_editor/api.py
+++ b/larray_editor/api.py
@@ -65,14 +65,15 @@ def get_title(obj, depth=0, maxnames=3):
     return ', '.join(names)
 
 
+# TODO: update doccstring
 def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth=0):
     """
     Opens a new editor window.
 
     Parameters
     ----------
-    obj : np.ndarray, LArray, Session, dict, str or REOPEN_LAST_FILE, optional
-        Object to visualize. If string, array(s) will be loaded from the file given as argument.
+    obj : (dict of) np.ndarray, LArray, Session, dict, str or REOPEN_LAST_FILE, optional
+        Object(s) to visualize. If string, array(s) will be loaded from the file given as argument.
         Passing the constant REOPEN_LAST_FILE loads the last opened file.
         Defaults to the collection of all local variables where the function was called.
     title : str, optional
@@ -138,14 +139,16 @@ def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth
     restore_except_hook()
 
 
+# TODO: update doccstring
 def view(obj=None, title='', depth=0):
     """
     Opens a new viewer window. Arrays are loaded in readonly mode and their content cannot be modified.
 
     Parameters
     ----------
-    obj : np.ndarray, LArray, Session, dict or str, optional
-        Object to visualize. If string, array(s) will be loaded from the file given as argument.
+    obj : (dict of) np.ndarray, LArray, Session, dict, str or REOPEN_LAST_FILE, optional
+        Object(s) to visualize. If string, array(s) will be loaded from the file given as argument.
+        Passing the constant REOPEN_LAST_FILE loads the last opened file.
         Defaults to the collection of all local variables where the function was called.
     title : str, optional
         Title for the current object. Defaults to the name of the first object found in the caller namespace which
diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index ca3f6d5..f0354e0 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -3,6 +3,7 @@
 import matplotlib
 import numpy as np
 import collections
+from collections import OrderedDict
 
 from larray import LArray, Session, empty
 from larray_editor.utils import (PY2, PYQT5, _, create_action, show_figure, ima, commonpath, dependencies,
@@ -12,9 +13,10 @@
 
 from qtpy.QtCore import Qt, QUrl
 from qtpy.QtGui import QDesktopServices, QKeySequence
-from qtpy.QtWidgets import (QMainWindow, QWidget, QListWidget, QListWidgetItem, QSplitter, QFileDialog, QPushButton,
-                            QDialogButtonBox, QShortcut, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QUndoStack,
-                            QCheckBox, QComboBox, QMessageBox, QDialog, QInputDialog, QLabel, QGroupBox, QRadioButton)
+from qtpy.QtWidgets import (QMainWindow, QWidget, QTreeWidget, QTreeWidgetItem,
+                            QSplitter, QFileDialog, QPushButton, QDialogButtonBox, QShortcut,
+                            QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QUndoStack, QCheckBox,
+                            QComboBox, QMessageBox, QDialog, QInputDialog, QLabel, QGroupBox, QRadioButton)
 
 try:
     from qtconsole.rich_jupyter_widget import RichJupyterWidget
@@ -49,6 +51,8 @@
 # (long) strings are not handled correctly so should NOT be in this list
 # tuple, list
 DISPLAY_IN_GRID = (LArray, np.ndarray)
+EXPANDABLE_OBJ = (dict, Session)
+DISPLAY_IN_TREEWIDGET = EXPANDABLE_OBJ + DISPLAY_IN_GRID
 
 
 class AbstractEditor(QMainWindow):
@@ -278,6 +282,293 @@ def update_title(self):
         raise NotImplementedError()
 
 
+def _display_in_grid(k, v):
+    return not k.startswith('__') and isinstance(v, DISPLAY_IN_GRID)
+
+
+def _display_in_treewidget(k, v):
+    return not k.startswith('__') and isinstance(v, DISPLAY_IN_TREEWIDGET)
+
+
+class MapItem:
+    def __init__(self, obj, treeitem, parent=None):
+        """
+        Parameters
+        ----------
+        obj: (dict-like of) displayable object(s)
+        treeitem: QTreeWidgetItem
+        parent: MapItem
+        """
+        self._children = OrderedDict()
+        self.parent = parent
+        self.treeitem = treeitem
+        self.obj = obj
+
+    @property
+    def parent(self):
+        return self._parent
+
+    @parent.setter
+    def parent(self, parent):
+        assert parent is None or isinstance(parent, MapItem)
+        self._parent = parent
+
+    @property
+    def treeitem(self):
+        return self._treeitem
+
+    @treeitem.setter
+    def treeitem(self, treeitem):
+        assert isinstance(treeitem, QTreeWidgetItem)
+        self._treeitem = treeitem
+
+    @property
+    def obj(self):
+        return self._obj
+
+    @obj.setter
+    def obj(self, obj):
+        assert _display_in_treewidget('', obj)
+        self._obj = obj
+        if isinstance(obj, EXPANDABLE_OBJ):
+            self.add_children(obj)
+
+    def add_child(self, name, value):
+        """
+        Parameters
+        ----------
+        name: str
+        value: displayable obj
+        """
+        if _display_in_grid(name, value):
+            if name in self._children:
+                treeitem = self._children[name].treeitem
+            else:
+                treeitem = QTreeWidgetItem(self._treeitem, [name])
+                if isinstance(value, LArray):
+                    treeitem.setToolTip(0, str(value.info))
+            item = MapItem(value, treeitem, self)
+            self._children[name] = item
+        else:
+            item = None
+        return item
+
+    def add_children(self, children):
+        """
+        Parameters
+        ----------
+        children: expandable obj
+        """
+        assert isinstance(children, EXPANDABLE_OBJ)
+        for k, v in children.items():
+            self.add_child(k, v)
+
+    def take_child(self, name):
+        """
+        Parameters
+        ----------
+        name: str
+        """
+        assert name in self._children
+        child = self._children[name]
+        self._treeitem.removeChild(child.treeitem)
+        del self._children[name]
+        return child
+
+    def get_child(self, name):
+        """
+        Parameters
+        ----------
+        name: str
+        """
+        return self._children.get(name)
+
+    def child_count(self):
+        return len(self._children)
+
+
+class MapItems(OrderedDict):
+    def __init__(self, treewidget):
+        OrderedDict.__init__(self)
+        self._treewidget = treewidget
+
+    def set_items(self, data):
+        """
+        Parameters
+        ----------
+        data: OrderedDict
+        """
+        if not isinstance(data, OrderedDict):
+            data = OrderedDict(data)
+        # set the map
+        for k, v in data.items():
+            self.add_item(k, v)
+        # display the first array if any
+        if self._treewidget.topLevelItemCount():
+            self._treewidget.setCurrentItem(self._treewidget.topLevelItem(0))
+
+    def add_item(self, name, value, parent_name=None):
+        """
+        Parameters
+        ----------
+        name: str
+        value: (dict-like of) array-like object(s)
+        parent_name: str
+        """
+        if _display_in_treewidget(name, value):
+            # displayable object
+            if isinstance(value, DISPLAY_IN_GRID):
+                if parent_name is not None:
+                    parent_item = self[parent_name]
+                    parent_item.add_child(name, value)
+                else:
+                    if name in self:
+                        # update existing item
+                        self[name].obj = value
+                    else:
+                        # add new item
+                        treeitem = QTreeWidgetItem([name])
+                        if isinstance(value, LArray):
+                            treeitem.setToolTip(0, str(value.info))
+                        self._treewidget.addTopLevelItem(treeitem)
+                        self[name] = MapItem(value, treeitem)
+            # dict-like object
+            else:
+                if name in self:
+                    # update existing item
+                    self[name].obj = value
+                else:
+                    # add new item
+                    treeitem = QTreeWidgetItem([name])
+                    self._treewidget.addTopLevelItem(treeitem)
+                    treeitem.setExpanded(True)
+                    self[name] = MapItem(value, treeitem)
+
+    def take_item(self, name, parent_name=None):
+        """
+        Parameters
+        ----------
+        name: str
+        parent_name: str
+        """
+        if parent_name is not None:
+            assert parent_name in self
+            parent_item = self[parent_name]
+            item = parent_item.take_child(name)
+        else:
+            assert name in self
+            item = self[name]
+            index = self._treewidget.indexOfTopLevelItem(item.treeitem)
+            self._treewidget.takeTopLevelItem(index)
+            del self[name]
+        return item
+
+    def update_mapping(self, objects):
+        _self_objects = self.to_map_objects()
+        # XXX: use ordered set so that the order is non-random if the underlying container is ordered?
+        keys_before = set(_self_objects.keys())
+        keys_after = set(objects.keys())
+        # Contains both new and keys for which the object id changed (but not deleted keys nor inplace modified keys).
+        # Inplace modified arrays should be already handled in ipython_cell_executed by the setitem_pattern.
+        changed_keys = [k for k in keys_after if objects[k] is not _self_objects.get(k)]
+
+        # when a key is re-assigned, it can switch from being displayable to non-displayable or vice versa
+        displayable_keys_before = set(k for k in keys_before if _display_in_treewidget(k, _self_objects[k]))
+        displayable_keys_after = set(k for k in keys_after if _display_in_treewidget(k, objects[k]))
+        deleted_displayable_keys = displayable_keys_before - displayable_keys_after
+        new_displayable_keys = displayable_keys_after - displayable_keys_before
+        # this can contain more keys than new_displayble_keys (because of existing keys which changed value)
+        changed_displayable_keys = [k for k in changed_keys if _display_in_treewidget(k, objects[k])]
+
+        # 1) deleted old keys
+        for k in deleted_displayable_keys:
+            self.take_item(k)
+        # 2) add new/modify existing keys
+        for k in changed_displayable_keys:
+            self.add_item(k, objects[k])
+
+        # 3) mark session as dirty if needed
+        if len(changed_displayable_keys) + len(deleted_displayable_keys) > 0:
+            self.unsaved_modifications = True
+
+        # 4) change displayed array in the array widget
+        # only display first result if there are more than one
+        if changed_displayable_keys:
+            to_display = changed_displayable_keys[0]
+            if not _display_in_grid(to_display, objects[to_display]):
+                to_display = None
+        else:
+            to_display = None
+        return to_display
+
+    def get_map_item(self, name, parent_name=None):
+        """
+        Parameters
+        ----------
+        name: str
+        parent_name: str
+
+        Returns
+        -------
+        MapItem
+        """
+        if parent_name is not None:
+            if parent_name not in self:
+                return None
+            parent_item = self[parent_name]
+            return parent_item.get_child(name)
+        else:
+            return self.get(name)
+
+    def get_object(self, name, parent_name=None):
+        """
+        Parameters
+        ----------
+        name: str
+        parent_name: str
+        """
+        item = self.get_map_item(name, parent_name)
+        if item is not None:
+            return item.obj
+
+    def get_tree_item(self, name, parent_name=None):
+        """
+        Parameters
+        ----------
+        name: str
+        parent_name: str
+
+        Returns
+        -------
+        QTreeWidgetItem
+        """
+        item = self.get_map_item(name, parent_name)
+        if item is not None:
+            return item.treeitem
+
+    def get_selected_item(self):
+        selected = self._treewidget.selectedItems()
+        if selected:
+            assert len(selected) == 1
+            selected_item = selected[0]
+            assert isinstance(selected_item, QTreeWidgetItem)
+            item_name = str(selected_item.text(0))
+            if selected_item.parent() is not None:
+                parent_name = str(selected_item.parent().text(0))
+            else:
+                parent_name = None
+            selected_item = self.get_map_item(item_name, parent_name)
+            return item_name, selected_item
+        else:
+            return (None, None)
+
+    def to_map_objects(self):
+        return OrderedDict([(k, v.obj) for k, v in self.items()])
+
+    def to_map_treeitems(self):
+        return OrderedDict([(k, v.treeitem) for k, v in self.items()])
+
+
 class MappingEditor(AbstractEditor):
     """Session Editor Dialog"""
 
@@ -295,7 +586,8 @@ def __init__(self, parent=None):
         self.current_array = None
         self.current_array_name = None
 
-        self._listwidget = None
+        self._treewidget = None
+        self._mapitems = None
         self.eval_box = None
         self.expressions = {}
         self.kernel = None
@@ -308,19 +600,21 @@ def _setup_and_check(self, widget, data, title, readonly):
         layout = QVBoxLayout()
         widget.setLayout(layout)
 
-        self._listwidget = QListWidget(self)
+        self._treewidget = QTreeWidget(self)
+        self._treewidget.headerItem().setHidden(True)
         # this is a bit more reliable than currentItemChanged which is not emitted when no item was selected before
-        self._listwidget.itemSelectionChanged.connect(self.on_selection_changed)
-        self._listwidget.setMinimumWidth(45)
+        self._treewidget.itemSelectionChanged.connect(self.on_selection_changed)
+        self._treewidget.setMinimumWidth(45)
 
-        del_item_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self._listwidget)
+        del_item_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self._treewidget)
         del_item_shortcut.activated.connect(self.delete_current_item)
 
-        self.data = Session()
         self.arraywidget = ArrayEditorWidget(self, readonly=readonly)
         self.arraywidget.dataChanged.connect(self.push_changes)
         self.arraywidget.model_data.dataChanged.connect(self.update_title)
 
+        self._mapitems = MapItems(self._treewidget)
+
         if qtconsole_available:
             # Create an in-process kernel
             kernel_manager = QtInProcessKernelManager()
@@ -384,7 +678,7 @@ def void_formatter(array, *args, **kwargs):
             right_panel_widget.setLayout(right_panel_layout)
 
         main_splitter = QSplitter(Qt.Horizontal)
-        main_splitter.addWidget(self._listwidget)
+        main_splitter.addWidget(self._treewidget)
         main_splitter.addWidget(right_panel_widget)
         main_splitter.setSizes([10, 90])
         main_splitter.setCollapsible(1, False)
@@ -396,7 +690,7 @@ def void_formatter(array, *args, **kwargs):
             if len(self.recent_data_files.files) > 0:
                 data = self.recent_data_file.files[0]
             else:
-                data = Session()
+                data = OrderedDict()
 
         # load file if any
         if isinstance(data, str):
@@ -405,28 +699,8 @@ def void_formatter(array, *args, **kwargs):
             else:
                 QMessageBox.critical(self, "Error", "File {} could not be found".format(data))
                 self.new()
-        # convert input data to Session if not
-        else:
-            self.data = data if isinstance(data, Session) else Session(data)
-            if qtconsole_available:
-                self.kernel.shell.push(dict(self.data.items()))
-            arrays = [k for k, v in self.data.items() if self._display_in_grid(k, v)]
-            self.add_list_items(arrays)
-        self._listwidget.setCurrentRow(0)
-
-    def _reset(self):
-        self.data = Session()
-        self._listwidget.clear()
-        self.current_array = None
-        self.current_array_name = None
-        self.edit_undo_stack.clear()
-        if qtconsole_available:
-            self.kernel.shell.reset()
-            self.kernel.shell.run_cell('from larray import *')
-            self.ipython_cell_executed()
         else:
-            self.eval_box.setText('None')
-            self.line_edit_update()
+            self.set_data(data)
 
     def _setup_file_menu(self, menu_bar):
         file_menu = menu_bar.addMenu('&File')
@@ -483,103 +757,92 @@ def unsaved_modifications(self, unsaved_modifications):
         self._unsaved_modifications = unsaved_modifications
         self.update_title()
 
-    def add_list_item(self, name):
-        listitem = QListWidgetItem(self._listwidget)
-        listitem.setText(name)
-        value = self.data[name]
-        if isinstance(value, LArray):
-            listitem.setToolTip(str(value.info))
-
-    def add_list_items(self, names):
-        for name in names:
-            self.add_list_item(name)
-
-    def delete_list_item(self, to_delete):
-        deleted_items = self._listwidget.findItems(to_delete, Qt.MatchExactly)
-        assert len(deleted_items) == 1
-        deleted_item_idx = self._listwidget.row(deleted_items[0])
-        self._listwidget.takeItem(deleted_item_idx)
-
-    def select_list_item(self, to_display):
-        changed_items = self._listwidget.findItems(to_display, Qt.MatchExactly)
-        assert len(changed_items) == 1
-        prev_selected = self._listwidget.selectedItems()
-        assert len(prev_selected) <= 1
-        # if the currently selected item (value) need to be refreshed (e.g it was modified)
-        if prev_selected and prev_selected[0] == changed_items[0]:
-            # we need to update the array widget explicitly
-            self.set_current_array(self.data[to_display], to_display)
+    def _reset(self):
+        self._treewidget.clear()
+        self._mapitems = MapItems(self._treewidget)
+        self.current_array = None
+        self.current_array_name = None
+        self.edit_undo_stack.clear()
+        if qtconsole_available:
+            self.kernel.shell.reset()
+            self.kernel.shell.run_cell('from larray import *')
+            self.ipython_cell_executed()
         else:
-            self._listwidget.setCurrentItem(changed_items[0])
-
-    def update_mapping(self, value):
-        # XXX: use ordered set so that the order is non-random if the underlying container is ordered?
-        keys_before = set(self.data.keys())
-        keys_after = set(value.keys())
-        # Contains both new and keys for which the object id changed (but not deleted keys nor inplace modified keys).
-        # Inplace modified arrays should be already handled in ipython_cell_executed by the setitem_pattern.
-        changed_keys = [k for k in keys_after if value[k] is not self.data.get(k)]
-
-        # when a key is re-assigned, it can switch from being displayable to non-displayable or vice versa
-        displayable_keys_before = set(k for k in keys_before if self._display_in_grid(k, self.data[k]))
-        displayable_keys_after = set(k for k in keys_after if self._display_in_grid(k, value[k]))
-        deleted_displayable_keys = displayable_keys_before - displayable_keys_after
-        new_displayable_keys = displayable_keys_after - displayable_keys_before
-        # this can contain more keys than new_displayble_keys (because of existing keys which changed value)
-        changed_displayable_keys = [k for k in changed_keys if self._display_in_grid(k, value[k])]
+            self.eval_box.setText('None')
+            self.line_edit_update()
 
-        # 1) update session/mapping
-        # a) deleted old keys
-        for k in keys_before - keys_after:
-            del self.data[k]
-        # b) add new/modify existing keys
-        for k in changed_keys:
-            self.data[k] = value[k]
+    def set_data(self, data):
+        """
+        Parameters
+        ----------
+        data: dict-like
+        """
+        assert hasattr(data, 'keys')
+        self._reset()
+        if not isinstance(data, OrderedDict):
+            data = OrderedDict(data)
+        if qtconsole_available:
+            self.kernel.shell.push(data)
+        self._mapitems.set_items(data)
 
-        # 2) update list widget
-        for k in deleted_displayable_keys:
-            self.delete_list_item(k)
-        self.add_list_items(new_displayable_keys)
+    def delete_current_item(self):
+        current_item = self._treewidget.currentItem()
+        name = current_item.text(0)
+        parent_name = str(current_item.parent().text(0)) if current_item.parent() is not None else None
+        # delete in tree view
+        item = self._mapitems.take_item(name, parent_name)
+        # delete in kernel
+        if qtconsole_available:
+            if parent_name is not None:
+                parent_obj = item.parent.obj
+                del parent_obj[name]
+                self.kernel.shell.push({parent_name: parent_obj})
+            else:
+                self.kernel.shell.del_var(name)
+        self.unsaved_modifications = True
 
-        # 3) mark session as dirty if needed
-        if len(changed_displayable_keys) > 0 or deleted_displayable_keys:
-            self.unsaved_modifications = True
+    def select_array_item(self, to_display, parent_name=None):
+        """
+        Parameters
+        ----------
+        to_display: str
+        parent_name: str
+        """
+        array_item = self._mapitems.get_map_item(to_display, parent_name)
+        prev_selected = self._treewidget.selectedItems()
+        assert len(prev_selected) <= 1
+        # if the currently selected item (value) need to be refreshed (e.g it was modified)
+        if prev_selected and prev_selected[0] == array_item:
+            # we need to update the array widget explicitly
+            self.set_current_array(array_item.obj, to_display)
+        else:
+            self._treewidget.setCurrentItem(array_item.treeitem)
 
-        # 4) change displayed array in the array widget
-        # only display first result if there are more than one
-        to_display = changed_displayable_keys[0] if changed_displayable_keys else None
+    def update_mapping(self, objects):
+        to_display = self._mapitems.update_mapping(objects)
         if to_display is not None:
-            self.select_list_item(to_display)
+            self.select_array_item(to_display)
         return to_display
 
-    def delete_current_item(self):
-        current_item = self._listwidget.currentItem()
-        name = str(current_item.text())
-        del self.data[name]
-        if qtconsole_available:
-            self.kernel.shell.del_var(name)
-        self.unsaved_modifications = True
-        self._listwidget.takeItem(self._listwidget.row(current_item))
-
     def line_edit_update(self):
         import larray as la
         s = self.eval_box.text()
+        map_objects = OrderedDict([(k, i.obj) for k, i in self._mapitems.items()])
         if assignment_pattern.match(s):
-            context = self.data._objects.copy()
+            context = map_objects.copy()
             exec(s, la.__dict__, context)
             varname = self.update_mapping(context)
             if varname is not None:
                 self.expressions[varname] = s
         else:
-            self.view_expr(eval(s, la.__dict__, self.data))
+            self.view_expr(eval(s, la.__dict__, map_objects))
 
     def view_expr(self, array):
-        self._listwidget.clearSelection()
+        self._treewidget.clearSelection()
         self.set_current_array(array, '<expr>')
 
-    def _display_in_grid(self, k, v):
-        return not k.startswith('__') and isinstance(v, DISPLAY_IN_GRID)
-
+    # TODO: find a way to detect when an array is added to/deleted from a Session or modified
+    # TODO: find a way to detect when an array (from the user namespace) is modified
     def ipython_cell_executed(self):
         user_ns = self.kernel.shell.user_ns
         ip_keys = set(['In', 'Out', '_', '__', '___',
@@ -599,17 +862,17 @@ def ipython_cell_executed(self):
             varname = m.group(1)
             # otherwise it should have failed at this point, but let us be sure
             if varname in clean_ns:
-                if self._display_in_grid(varname, clean_ns[varname]):
+                if _display_in_grid(varname, clean_ns[varname]):
                     # XXX: this completely refreshes the array, including detecting scientific & ndigits, which might
                     # not be what we want in this case
-                    self.select_list_item(varname)
+                    self.select_array_item(varname)
         else:
             # not setitem => assume expr or normal assignment
             if last_input in clean_ns:
-                # the name exists in the session (variable)
-                if self._display_in_grid('', self.data[last_input]):
+                # the name exists in the default session (variable)
+                if _display_in_grid('', clean_ns[last_input]):
                     # select and display it
-                    self.select_list_item(last_input)
+                    self.select_array_item(last_input)
             else:
                 # any statement can contain a call to a function which updates globals
                 # this will select (or refresh) the "first" changed array
@@ -622,7 +885,7 @@ def ipython_cell_executed(self):
                 # last command. Which means that if the last command did not produce any output, _ is not modified.
                 cur_output = user_ns['_oh'].get(cur_input_num)
                 if cur_output is not None:
-                    if self._display_in_grid('_', cur_output):
+                    if _display_in_grid('_', cur_output):
                         self.view_expr(cur_output)
 
                     if isinstance(cur_output, collections.Iterable):
@@ -632,14 +895,9 @@ def ipython_cell_executed(self):
                         show_figure(self, cur_output.figure)
 
     def on_selection_changed(self):
-        selected = self._listwidget.selectedItems()
-        if selected:
-            assert len(selected) == 1
-            selected_item = selected[0]
-            assert isinstance(selected_item, QListWidgetItem)
-            name = str(selected_item.text())
-            array = self.data[name]
-            self.set_current_array(array, name)
+        name, item = self._mapitems.get_selected_item()
+        if item is not None and isinstance(item.obj, DISPLAY_IN_GRID):
+            self.set_current_array(item.obj, name)
             expr = self.expressions.get(name, name)
             if qtconsole_available:
                 # this does not work because it updates the NEXT input, not the
@@ -681,13 +939,6 @@ def set_current_file(self, filepath):
         self.current_file = filepath
         self.update_title()
 
-    def _add_arrays(self, arrays):
-        for k, v in arrays.items():
-            self.data[k] = v
-            self.add_list_item(k)
-        if qtconsole_available:
-            self.kernel.shell.push(dict(arrays))
-
     def _ask_to_save_if_unsaved_modifications(self):
         """
         Returns
@@ -921,8 +1172,9 @@ def save_script(self):
     #  METHODS TO SAVE/LOAD DATA  #
     #=============================#
 
+    # TODO: implement _open_directory or _open_files in case we want to load several sessions (and additional arrays)
     def _open_file(self, filepath):
-        session = Session()
+        data = Session()
         # a list => .csv files. Possibly a single .csv file.
         if isinstance(filepath, (list, tuple)):
             fpaths = filepath
@@ -942,10 +1194,8 @@ def _open_file(self, filepath):
             current_file_name = filepath
             display_name = os.path.basename(filepath)
         try:
-            session.load(filepath, names)
-            self._reset()
-            self._add_arrays(session)
-            self._listwidget.setCurrentRow(0)
+            data.load(filepath, names)
+            self.set_data(data)
             self.set_current_file(current_file_name)
             self.unsaved_modifications = False
             self.statusBar().showMessage("Loaded: {}".format(display_name), 4000)
@@ -953,6 +1203,7 @@ def _open_file(self, filepath):
             QMessageBox.critical(self, "Error", "Something went wrong during load of file(s) {}:\n{}"
                                  .format(display_name, e))
 
+    # TODO: find a way to load several sessions at once
     def open_data(self):
         if self._ask_to_save_if_unsaved_modifications():
             filter = "All (*.xls *xlsx *.h5 *.csv);;Excel Files (*.xls *xlsx);;HDF Files (*.h5);;CSV Files (*.csv)"
@@ -974,15 +1225,16 @@ def open_recent_file(self):
         if self._ask_to_save_if_unsaved_modifications():
             action = self.sender()
             if action:
-                filepath = action.data()
+                filepath = action._mapitems()
                 if os.path.exists(filepath):
                     self._open_file(filepath)
                 else:
                     QMessageBox.warning(self, "Warning", "File {} could not be found".format(filepath))
 
+    # TODO: find a way to save several sessions (and additional arrays) at once
     def _save_data(self, filepath):
         try:
-            session = Session({k: v for k, v in self.data.items() if self._display_in_grid(k, v)})
+            session = Session({k: v for k, v in self._mapitems.items() if _display_in_grid(k, v)})
             session.save(filepath)
             self.set_current_file(filepath)
             self.edit_undo_stack.clear()

From d5580da061547ad9066fb44661c8e648ed905b01 Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Tue, 14 May 2019 11:31:10 +0200
Subject: [PATCH 4/8] - removed setitem_pattern - added getitem_pattern and
 getattr_pattern - updated ipython_cell_executed()

---
 larray_editor/editor.py | 89 ++++++++++++++++++++++++++---------------
 1 file changed, 56 insertions(+), 33 deletions(-)

diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index f0354e0..536d341 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -45,7 +45,8 @@
 REOPEN_LAST_FILE = object()
 
 assignment_pattern = re.compile('[^\[\]]+[^=]=[^=].+')
-setitem_pattern = re.compile('(.+)\[.+\][^=]=[^=].+')
+getitem_pattern = re.compile('(\w+)\[(.+?)\].*')
+getattr_pattern = re.compile('(\w+)\.(\w+).*')
 history_vars_pattern = re.compile('_i?\d+')
 # XXX: add all scalars except strings (from numpy or plain Python)?
 # (long) strings are not handled correctly so should NOT be in this list
@@ -857,42 +858,64 @@ def ipython_cell_executed(self):
         # 'In' and '_ih' point to the same object (but '_ih' is supposed to be the non-overridden one)
         cur_input_num = len(user_ns['_ih']) - 1
         last_input = user_ns['_ih'][-1]
-        if setitem_pattern.match(last_input):
-            m = setitem_pattern.match(last_input)
+        print('last_input', last_input)
+        last_input = last_input.strip()
+
+        # check if simply selecting a displayable object in grid
+        if last_input in clean_ns:
+            # the name exists in the default session (variable)
+            if _display_in_grid('', clean_ns[last_input]):
+                # select and display it
+                self.select_array_item(last_input)
+            return
+
+        # check if expression of the kind '<varname>[(...)] (...)' or '<varname>.<attribute> (...)'
+        varname = itemname = None
+        m = getitem_pattern.match(last_input)
+        if m:
             varname = m.group(1)
+            itemname = m.group(2).replace("'", "").replace('"', '')
+        m = getattr_pattern.match(last_input)
+        if m:
+            varname = m.group(1)
+            itemname = m.group(2)
+
+        if varname:
             # otherwise it should have failed at this point, but let us be sure
             if varname in clean_ns:
-                if _display_in_grid(varname, clean_ns[varname]):
-                    # XXX: this completely refreshes the array, including detecting scientific & ndigits, which might
-                    # not be what we want in this case
-                    self.select_array_item(varname)
+                var = clean_ns[varname]
+                if _display_in_treewidget(varname, var):
+                    # check if var is a dictionary or session
+                    if isinstance(var, EXPANDABLE_OBJ):
+                        if itemname in var.keys() and _display_in_grid(itemname, var[itemname]):
+                            self.select_array_item(itemname, varname)
+                        else:
+                            self.update_mapping(clean_ns)
+                    else:
+                        # XXX: this completely refreshes the array, including detecting scientific & ndigits,
+                        # which might not be what we want in this case
+                        self.select_array_item(varname)
         else:
-            # not setitem => assume expr or normal assignment
-            if last_input in clean_ns:
-                # the name exists in the default session (variable)
-                if _display_in_grid('', clean_ns[last_input]):
-                    # select and display it
-                    self.select_array_item(last_input)
-            else:
-                # any statement can contain a call to a function which updates globals
-                # this will select (or refresh) the "first" changed array
-                self.update_mapping(clean_ns)
-
-                # if the statement produced any output (probably because it is a simple expression), display it.
-
-                # _oh and Out are supposed to be synonyms but "_ih" is supposed to be the non-overridden one.
-                # It would be easier to use '_' instead but that refers to the last output, not the output of the
-                # last command. Which means that if the last command did not produce any output, _ is not modified.
-                cur_output = user_ns['_oh'].get(cur_input_num)
-                if cur_output is not None:
-                    if _display_in_grid('_', cur_output):
-                        self.view_expr(cur_output)
-
-                    if isinstance(cur_output, collections.Iterable):
-                        cur_output = np.ravel(cur_output)[0]
-
-                    if isinstance(cur_output, matplotlib.axes.Subplot) and 'inline' not in matplotlib.get_backend():
-                        show_figure(self, cur_output.figure)
+            # not (get/set)(item/attribute) => assume expr or normal assignment
+            # any statement can contain a call to a function which updates globals
+            # this will select (or refresh) the "first" changed array
+            self.update_mapping(clean_ns)
+
+            # if the statement produced any output (probably because it is a simple expression), display it.
+
+            # _oh and Out are supposed to be synonyms but "_ih" is supposed to be the non-overridden one.
+            # It would be easier to use '_' instead but that refers to the last output, not the output of the
+            # last command. Which means that if the last command did not produce any output, _ is not modified.
+            cur_output = user_ns['_oh'].get(cur_input_num)
+            if cur_output is not None:
+                if _display_in_grid('_', cur_output):
+                    self.view_expr(cur_output)
+
+                if isinstance(cur_output, collections.Iterable):
+                    cur_output = np.ravel(cur_output)[0]
+
+                if isinstance(cur_output, matplotlib.axes.Subplot) and 'inline' not in matplotlib.get_backend():
+                    show_figure(self, cur_output.figure)
 
     def on_selection_changed(self):
         name, item = self._mapitems.get_selected_item()

From bef4f440001a13bd99ecdff00aedc40355461b81 Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Tue, 14 May 2019 12:47:52 +0200
Subject: [PATCH 5/8] removed print statement

---
 larray_editor/editor.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index 536d341..00cdf71 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -858,7 +858,6 @@ def ipython_cell_executed(self):
         # 'In' and '_ih' point to the same object (but '_ih' is supposed to be the non-overridden one)
         cur_input_num = len(user_ns['_ih']) - 1
         last_input = user_ns['_ih'][-1]
-        print('last_input', last_input)
         last_input = last_input.strip()
 
         # check if simply selecting a displayable object in grid

From 2728fe481707d963688617a67a8e5fbc21e1d901 Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Tue, 14 May 2019 14:59:57 +0200
Subject: [PATCH 6/8] updated condition in ipython_cell_executed()

---
 larray_editor/editor.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index 00cdf71..9fbe261 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -886,7 +886,8 @@ def ipython_cell_executed(self):
                 if _display_in_treewidget(varname, var):
                     # check if var is a dictionary or session
                     if isinstance(var, EXPANDABLE_OBJ):
-                        if itemname in var.keys() and _display_in_grid(itemname, var[itemname]):
+                        if '=' not in last_input and itemname in var.keys() \
+                                and _display_in_grid(itemname, var[itemname]):
                             self.select_array_item(itemname, varname)
                         else:
                             self.update_mapping(clean_ns)

From 749b034446e13209ab414b3994e91b6c8bb320af Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Wed, 15 May 2019 14:55:09 +0200
Subject: [PATCH 7/8] force update of dict-like objects when the operator =
 appears in the last executed command

---
 larray_editor/editor.py | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index 9fbe261..4ce57d0 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -464,14 +464,19 @@ def take_item(self, name, parent_name=None):
             del self[name]
         return item
 
-    def update_mapping(self, objects):
+    def update_mapping(self, objects, changed_expandable_obj_keys=None):
         _self_objects = self.to_map_objects()
         # XXX: use ordered set so that the order is non-random if the underlying container is ordered?
         keys_before = set(_self_objects.keys())
         keys_after = set(objects.keys())
         # Contains both new and keys for which the object id changed (but not deleted keys nor inplace modified keys).
-        # Inplace modified arrays should be already handled in ipython_cell_executed by the setitem_pattern.
+        # Inplace modified arrays should be already handled in ipython_cell_executed.
         changed_keys = [k for k in keys_after if objects[k] is not _self_objects.get(k)]
+        # objects and _self_objects contain references to the same expandable objects in memory so
+        # we have no way to know if an expandable object has been modified except by checking if
+        # the operator '=' has been used in the console
+        if changed_expandable_obj_keys:
+            changed_keys += changed_expandable_obj_keys
 
         # when a key is re-assigned, it can switch from being displayable to non-displayable or vice versa
         displayable_keys_before = set(k for k in keys_before if _display_in_treewidget(k, _self_objects[k]))
@@ -819,8 +824,8 @@ def select_array_item(self, to_display, parent_name=None):
         else:
             self._treewidget.setCurrentItem(array_item.treeitem)
 
-    def update_mapping(self, objects):
-        to_display = self._mapitems.update_mapping(objects)
+    def update_mapping(self, objects, changed_expandable_obj_keys=None):
+        to_display = self._mapitems.update_mapping(objects, changed_expandable_obj_keys)
         if to_display is not None:
             self.select_array_item(to_display)
         return to_display
@@ -886,9 +891,12 @@ def ipython_cell_executed(self):
                 if _display_in_treewidget(varname, var):
                     # check if var is a dictionary or session
                     if isinstance(var, EXPANDABLE_OBJ):
-                        if '=' not in last_input and itemname in var.keys() \
-                                and _display_in_grid(itemname, var[itemname]):
-                            self.select_array_item(itemname, varname)
+                        if itemname in var.keys() and _display_in_grid(itemname, var[itemname]):
+                            if '=' not in last_input:
+                                self.select_array_item(itemname, varname)
+                            else:
+                                # force to update object
+                                self.update_mapping(clean_ns, changed_expandable_obj_keys=[varname])
                         else:
                             self.update_mapping(clean_ns)
                     else:

From f1cece4cc9f4a00fc1535f78220ef0d7c2750c09 Mon Sep 17 00:00:00 2001
From: Alix Damman <ald@plan.be>
Date: Fri, 17 May 2019 16:33:18 +0200
Subject: [PATCH 8/8] updated patterns + ipython_cell_executed() + added
 test_regex.py module

---
 larray_editor/editor.py           | 36 ++++++++---------
 larray_editor/tests/test_regex.py | 65 +++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 20 deletions(-)
 create mode 100644 larray_editor/tests/test_regex.py

diff --git a/larray_editor/editor.py b/larray_editor/editor.py
index 4ce57d0..bb95741 100644
--- a/larray_editor/editor.py
+++ b/larray_editor/editor.py
@@ -45,8 +45,8 @@
 REOPEN_LAST_FILE = object()
 
 assignment_pattern = re.compile('[^\[\]]+[^=]=[^=].+')
-getitem_pattern = re.compile('(\w+)\[(.+?)\].*')
-getattr_pattern = re.compile('(\w+)\.(\w+).*')
+setitem_pattern = re.compile('(\w+)\[(.+?)\].*[^=]=[^=].+')
+setattr_pattern = re.compile('(\w+)\.(\w+)[^\w=]+=[^=].+')
 history_vars_pattern = re.compile('_i?\d+')
 # XXX: add all scalars except strings (from numpy or plain Python)?
 # (long) strings are not handled correctly so should NOT be in this list
@@ -873,38 +873,34 @@ def ipython_cell_executed(self):
                 self.select_array_item(last_input)
             return
 
-        # check if expression of the kind '<varname>[(...)] (...)' or '<varname>.<attribute> (...)'
-        varname = itemname = None
-        m = getitem_pattern.match(last_input)
+        # check if expression of the kind '<varname>[(...)] = (...)' or '<varname>.<attribute> = (...)'
+        varname = None
+        m = setitem_pattern.match(last_input)
         if m:
             varname = m.group(1)
-            itemname = m.group(2).replace("'", "").replace('"', '')
-        m = getattr_pattern.match(last_input)
+
+        m = setattr_pattern.match(last_input)
         if m:
             varname = m.group(1)
-            itemname = m.group(2)
 
         if varname:
             # otherwise it should have failed at this point, but let us be sure
             if varname in clean_ns:
                 var = clean_ns[varname]
                 if _display_in_treewidget(varname, var):
-                    # check if var is a dictionary or session
-                    if isinstance(var, EXPANDABLE_OBJ):
-                        if itemname in var.keys() and _display_in_grid(itemname, var[itemname]):
-                            if '=' not in last_input:
-                                self.select_array_item(itemname, varname)
-                            else:
-                                # force to update object
-                                self.update_mapping(clean_ns, changed_expandable_obj_keys=[varname])
-                        else:
-                            self.update_mapping(clean_ns)
-                    else:
+                    # check if displayable in grid
+                    if _display_in_grid(varname, var):
                         # XXX: this completely refreshes the array, including detecting scientific & ndigits,
                         # which might not be what we want in this case
                         self.select_array_item(varname)
+                    else:
+                        # maybe updating an existing array of a session/dict or
+                        # adding a new array to the session/dict
+                        self.update_mapping(clean_ns)
+                        arrayname = m.group(2).replace('"', '').replace("'", '')
+                        self.select_array_item(arrayname, varname)
         else:
-            # not (get/set)(item/attribute) => assume expr or normal assignment
+            # not set(item/attribute) => assume expr or normal assignment
             # any statement can contain a call to a function which updates globals
             # this will select (or refresh) the "first" changed array
             self.update_mapping(clean_ns)
diff --git a/larray_editor/tests/test_regex.py b/larray_editor/tests/test_regex.py
new file mode 100644
index 0000000..be21211
--- /dev/null
+++ b/larray_editor/tests/test_regex.py
@@ -0,0 +1,65 @@
+from larray_editor.editor import setitem_pattern, setattr_pattern
+
+
+def test_setitem():
+    # new array
+    input = 'data = ndtest(10)'
+    m = setitem_pattern.match(input)
+    assert m is None
+
+    # update array
+    input = 'data[:] = 0'
+    varname, selection = setitem_pattern.match(input).groups()
+    assert varname == 'data'
+    assert selection == ':'
+
+    # testing array
+    input = 'data[2010:2012] == data2[2010:2012]'
+    m = setitem_pattern.match(input)
+    assert m is None
+
+    # session - new array
+    input = 'ses["data"] = ndtest(10)'
+    varname, selection = setitem_pattern.match(input).groups()
+    assert varname == 'ses'
+    assert selection == '"data"'
+
+    # session - update array
+    input = 'ses["data"][:] = 0'
+    varname, selection = setitem_pattern.match(input).groups()
+    assert varname == 'ses'
+    assert selection == '"data"'
+
+    # session - testing array
+    input = 'ses["data"] == ses2["data"]'
+    m = setitem_pattern.match(input)
+    assert m is None
+
+
+def test_setattr():
+    # new array
+    input = 'data = ndtest(10)'
+    m = setattr_pattern.match(input)
+    assert m is None
+
+    # update array metadata
+    input = 'data.meta.title = "my array"'
+    m = setattr_pattern.match(input)
+    assert m is None
+
+    # session - new array
+    input = 'ses.data = ndtest(10)'
+    varname, attrname = setattr_pattern.match(input).groups()
+    assert varname == 'ses'
+    assert attrname == 'data'
+
+    # session - update array
+    input = 'ses.data[:] = 0'
+    varname, attrname = setattr_pattern.match(input).groups()
+    assert varname == 'ses'
+    assert attrname == 'data'
+
+    # session - update array metadata
+    input = 'ses.data.meta.title = "my array"'
+    m = setattr_pattern.match(input)
+    assert m is None