Repository: biolab/orange3

Issue:
Excel Writer does not save roles
**What's wrong?**

Excel writer (or at least the widget) does not save roles, e.g. meta, target. This makes it rather useless.

**How can we reproduce the problem?** 

Save zoo data as xlsx and `type` will no longer be a target.

**What's your environment?**
- Operating system: macOS
- Orange version: 3.30
- How you installed Orange: dmg



Discussion:

Resolving patch:
diff --git a/Orange/data/io.py b/Orange/data/io.py
--- a/Orange/data/io.py
+++ b/Orange/data/io.py
@@ -277,6 +277,7 @@ class ExcelReader(_BaseExcelReader):
     EXTENSIONS = ('.xlsx',)
     DESCRIPTION = 'Microsoft Excel spreadsheet'
     ERRORS = ("#VALUE!", "#DIV/0!", "#REF!", "#NUM!", "#NULL!", "#NAME?")
+    OPTIONAL_TYPE_ANNOTATIONS = True
 
     def __init__(self, filename):
         super().__init__(filename)
@@ -319,7 +320,7 @@ def _get_active_sheet(self) -> openpyxl.worksheet.worksheet.Worksheet:
             return self.workbook.active
 
     @classmethod
-    def write_file(cls, filename, data):
+    def write_file(cls, filename, data, with_annotations=False):
         vars = list(chain((ContinuousVariable('_w'),) if data.has_weights() else (),
                           data.domain.attributes,
                           data.domain.class_vars,
@@ -329,12 +330,20 @@ def write_file(cls, filename, data):
                                data.X,
                                data.Y if data.Y.ndim > 1 else data.Y[:, np.newaxis],
                                data.metas)
-        headers = cls.header_names(data)
+        names = cls.header_names(data)
+        headers = (names,)
+        if with_annotations:
+            types = cls.header_types(data)
+            flags = cls.header_flags(data)
+            headers = (names, types, flags)
+
         workbook = xlsxwriter.Workbook(filename)
         sheet = workbook.add_worksheet()
-        for c, header in enumerate(headers):
-            sheet.write(0, c, header)
-        for i, row in enumerate(zipped_list_data, 1):
+
+        for r, parts in enumerate(headers):
+            for c, part in enumerate(parts):
+                sheet.write(r, c, part)
+        for i, row in enumerate(zipped_list_data, len(headers)):
             for j, (fmt, v) in enumerate(zip(formatters, flatten(row))):
                 sheet.write(i, j, fmt(v))
         workbook.close()
diff --git a/Orange/widgets/data/owsave.py b/Orange/widgets/data/owsave.py
--- a/Orange/widgets/data/owsave.py
+++ b/Orange/widgets/data/owsave.py
@@ -20,7 +20,7 @@ class OWSave(OWSaveBase):
     category = "Data"
     keywords = ["export"]
 
-    settings_version = 2
+    settings_version = 3
 
     class Inputs:
         data = Input("Data", Table)
@@ -120,6 +120,12 @@ def migrate_to_version_2():
         if version < 2:
             migrate_to_version_2()
 
+        if version < 3:
+            if settings.get("add_type_annotations") and \
+                    settings.get("stored_name") and \
+                    os.path.splitext(settings["stored_name"])[1] == ".xlsx":
+                settings["add_type_annotations"] = False
+
     def initial_start_dir(self):
         if self.filename and os.path.exists(os.path.split(self.filename)[0]):
             return self.filename


Test patch:
diff --git a/Orange/tests/test_xlsx_reader.py b/Orange/tests/test_xlsx_reader.py
--- a/Orange/tests/test_xlsx_reader.py
+++ b/Orange/tests/test_xlsx_reader.py
@@ -4,6 +4,7 @@
 import unittest
 import os
 from functools import wraps
+from tempfile import mkstemp
 from typing import Callable
 
 import numpy as np
@@ -46,6 +47,35 @@ def test_read_round_floats(self):
         self.assertIsInstance(domain[1], ContinuousVariable)
         self.assertEqual(domain[2].values, ("1", "2"))
 
+    def test_write_file(self):
+        fd, filename = mkstemp(suffix=".xlsx")
+        os.close(fd)
+
+        data = Table("zoo")
+        io.ExcelReader.write_file(filename, data, with_annotations=True)
+
+        reader = io.ExcelReader(filename)
+        read_data = reader.read()
+
+        domain1 = data.domain
+        domain2 = read_data.domain
+        self.assertEqual(len(domain1.attributes), len(domain2.attributes))
+        self.assertEqual(len(domain1.class_vars), len(domain2.class_vars))
+        self.assertEqual(len(domain1.metas), len(domain2.metas))
+        for var1, var2 in zip(domain1.variables + domain1.metas,
+                              domain2.variables + domain2.metas):
+            self.assertEqual(type(var1), type(var2))
+            self.assertEqual(var1.name, var2.name)
+            if var1.is_discrete:
+                self.assertEqual(var1.values, var2.values)
+
+        np.testing.assert_array_equal(data.X, read_data.X)
+        np.testing.assert_array_equal(data.Y, read_data.Y)
+        np.testing.assert_array_equal(data.metas, read_data.metas)
+        np.testing.assert_array_equal(data.W, read_data.W)
+
+        os.unlink(filename)
+
 
 class TestExcelHeader0(unittest.TestCase):
     @test_xlsx_xls
diff --git a/Orange/widgets/data/tests/test_owsave.py b/Orange/widgets/data/tests/test_owsave.py
--- a/Orange/widgets/data/tests/test_owsave.py
+++ b/Orange/widgets/data/tests/test_owsave.py
@@ -374,6 +374,31 @@ def test_migration_to_version_2(self):
         OWSave.migrate_settings(settings)
         self.assertTrue(settings["filter"] in OWSave.get_filters())
 
+    def test_migration_to_version_3(self):
+        settings = {"add_type_annotations": True,
+                    "stored_name": "zoo.xlsx",
+                    "__version__": 2}
+        widget = self.create_widget(OWSave, stored_settings=settings)
+        self.assertFalse(widget.add_type_annotations)
+
+        settings = {"add_type_annotations": True,
+                    "stored_name": "zoo.tab",
+                    "__version__": 2}
+        widget = self.create_widget(OWSave, stored_settings=settings)
+        self.assertTrue(widget.add_type_annotations)
+
+        settings = {"add_type_annotations": False,
+                    "stored_name": "zoo.xlsx",
+                    "__version__": 2}
+        widget = self.create_widget(OWSave, stored_settings=settings)
+        self.assertFalse(widget.add_type_annotations)
+
+        settings = {"add_type_annotations": False,
+                    "stored_name": "zoo.tab",
+                    "__version__": 2}
+        widget = self.create_widget(OWSave, stored_settings=settings)
+        self.assertFalse(widget.add_type_annotations)
+
 
 class TestFunctionalOWSave(WidgetTest):
     def setUp(self):