Repository: biolab/orange3

Issue:
Group By: KeyError with newly added Variables
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


**What's wrong?**
If first a subset of a table is given to group by (here: only sepal length with Select Columns), and then the remaining variables are added, the aggregations column in the aggregate table is empty. When clicking on the row, a KeyError exception occurs. 


![image](https://user-images.githubusercontent.com/67997227/150759199-854ac915-9e8a-4b5e-af7b-3a2aa3b583a0.png)


```
----------------------------- KeyError Exception ------------------------------
Traceback (most recent call last):
  File "D:\dev\Python\orange3\orange3\Orange\widgets\data\owgroupby.py", line 385, in __rows_selected
    active_aggregations = [self.aggregations[attr] for attr in selected_attrs]
  File "D:\dev\Python\orange3\orange3\Orange\widgets\data\owgroupby.py", line 385, in <listcomp>
    active_aggregations = [self.aggregations[attr] for attr in selected_attrs]
KeyError: ContinuousVariable(name='sepal width', number_of_decimals=1)
-------------------------------------------------------------------------------
```



**How can we reproduce the problem?** 

- see Screenshot
- insert this test in TestOWGropBy in Orange/widgets/data/tests/test_owgroupby.py

```
    def test_reproduce_bug(self):
        partial_domain = Domain([ContinuousVariable("a")])
        partial_table = Table.from_table(partial_domain, self.data)

        self.send_signal(self.widget.Inputs.data, partial_table)
        assert len(self.widget.aggregations) == 1
        self.assert_aggregations_equal(["Mean"])

        self.send_signal(self.widget.Inputs.data, self.data)
        assert len(self.widget.aggregations) == 1  # should be 5
        self.assert_aggregations_equal(['Mean', '', '', '', ''])  # should be defaults, not ''

        # select a new variable - should not raise
        self.select_table_rows(self.widget.agg_table_view, [1])
```

**Analysis and related problem**

A ContextSetting is created with the partial table.
With the full table, this context is recognized and the default values of the new variables in self.aggregations (owgroupby) are completely overwritten with the ContextSetting of the partial table.
In self.agg_table_view are all variables, but in self.aggregations only one. KeyError.


If a ContextSetting is a dict, it completely replaces the current setting:

https://github.com/biolab/orange-widget-base/blob/51d4f35e2fe807d617aac0547532d220d8942679/orangewidget/settings.py#L201

Wouldn't it be better if:
- Keys existing in the current setting and in the ContextSetting are updated by the ContextSetting.
- Keys existing in the current settings that are not in the ContextSetting should be kept.


**What's your environment?**

- Operating system: Windows 10
- Orange version: 3.32.0.dev
- How you installed Orange: clone github



Discussion:
Thanks for this excellent report. How I wish all reports were so detailed, and also included some debugging and even a test! :)

You are right, of course.

A note to whomever who'd work on this: this shouldn't be too difficult. Copy the idea from widgets that already do it properly, for instance, Discretize and Edit Domain. The latter is better, because it stores changes immediately, not through `storeSpecificSettings`.

Resolving patch:
diff --git a/Orange/widgets/data/owgroupby.py b/Orange/widgets/data/owgroupby.py
--- a/Orange/widgets/data/owgroupby.py
+++ b/Orange/widgets/data/owgroupby.py
@@ -443,9 +443,14 @@ def set_data(self, data: Table) -> None:
             if data
             else {}
         )
+        default_aggregations = self.aggregations.copy()
 
         self.openContext(self.data)
 
+        # restore aggregations
+        self.aggregations.update({k: v for k, v in default_aggregations.items()
+                                  if k not in self.aggregations})
+
         # update selections in widgets and re-plot
         self.agg_table_model.set_domain(data.domain if data else None)
         self._set_gb_selection()


Test patch:
diff --git a/Orange/widgets/data/tests/test_owgroupby.py b/Orange/widgets/data/tests/test_owgroupby.py
--- a/Orange/widgets/data/tests/test_owgroupby.py
+++ b/Orange/widgets/data/tests/test_owgroupby.py
@@ -20,7 +20,7 @@
 from Orange.widgets.tests.base import WidgetTest
 
 
-class TestOWGropBy(WidgetTest):
+class TestOWGroupBy(WidgetTest):
     def setUp(self) -> None:
         self.widget = self.create_widget(OWGroupBy)
         self.iris = Table("iris")
@@ -46,6 +46,14 @@ def test_data(self):
         self.send_signal(self.widget.Inputs.data, None)
         self.assertIsNone(self.get_output(self.widget.Outputs.data))
 
+    def test_data_domain_changed(self):
+        self.send_signal(self.widget.Inputs.data, self.iris[:, -2:])
+        self.assert_aggregations_equal(["Mean", "Concatenate"])
+
+        self.send_signal(self.widget.Inputs.data, self.iris[:, -3:])
+        self.assert_aggregations_equal(["Mean", "Mean", "Concatenate"])
+        self.select_table_rows(self.widget.agg_table_view, [0])
+
     @staticmethod
     def _set_selection(view: QListView, indices: List[int]):
         view.clearSelection()