diff --git a/Orange/data/io.py b/Orange/data/io.py index 06931613957..d181f6454b7 100644 --- 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/tests/test_xlsx_reader.py b/Orange/tests/test_xlsx_reader.py index 46f89ef36d9..feb913a112c 100644 --- 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/owsave.py b/Orange/widgets/data/owsave.py index 80fe7f948a6..b1a1ca70765 100644 --- 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 diff --git a/Orange/widgets/data/tests/test_owsave.py b/Orange/widgets/data/tests/test_owsave.py index de52ccd6cc6..2082c6efc44 100644 --- 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):