From c77b094b739f7a22e8c3b19088c7598b7c42cd6d Mon Sep 17 00:00:00 2001 From: janezd Date: Thu, 19 Sep 2024 17:41:44 +0200 Subject: [PATCH 1/2] File: Allow selecting a file with an arbitrary extension --- Orange/widgets/data/owfile.py | 87 ++++++++---- .../data/tests/actually-a-tab-file.xlsx | 27 ++++ .../widgets/data/tests/an_excel_file-too.foo | Bin 0 -> 8932 bytes Orange/widgets/data/tests/an_excel_file.foo | Bin 0 -> 8932 bytes Orange/widgets/data/tests/an_excel_file.xlsx | Bin 0 -> 8932 bytes Orange/widgets/data/tests/test_owfile.py | 128 ++++++++++++++++-- i18n/si/msgs.jaml | 7 +- 7 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 Orange/widgets/data/tests/actually-a-tab-file.xlsx create mode 100644 Orange/widgets/data/tests/an_excel_file-too.foo create mode 100644 Orange/widgets/data/tests/an_excel_file.foo create mode 100644 Orange/widgets/data/tests/an_excel_file.xlsx diff --git a/Orange/widgets/data/owfile.py b/Orange/widgets/data/owfile.py index e23a249b3e5..227df179301 100644 --- a/Orange/widgets/data/owfile.py +++ b/Orange/widgets/data/owfile.py @@ -38,7 +38,7 @@ # module's namespace so that old saved settings still work from Orange.widgets.utils.filedialogs import RecentPath -DEFAULT_READER_TEXT = "Automatically detect type" +DEFAULT_READER_TEXT = "Determine type from the file extension" log = logging.getLogger(__name__) @@ -147,8 +147,11 @@ class Warning(widget.OWWidget.Warning): class Error(widget.OWWidget.Error): file_not_found = Msg("File not found.") missing_reader = Msg("Missing reader.") + select_file_type = Msg("Select file type.") sheet_error = Msg("Error listing available sheets.") unknown = Msg("Read error:\n{}") + unknown_select = Msg( + "Read error, possibly due to incorrect choice of file type:\n{}") UserAdviceMessages = [ widget.Message( @@ -264,7 +267,7 @@ def package(w): self.reader_combo = QComboBox(self) self.reader_combo.setSizePolicy(Policy.Expanding, Policy.Fixed) self.reader_combo.setMinimumSize(QSize(100, 1)) - self.reader_combo.activated[int].connect(self.select_reader) + self.reader_combo.activated[int].connect(self.on_reader_change) box.layout().addWidget(self.reader_combo) layout.addWidget(box, 0, 1) @@ -327,6 +330,10 @@ def select_sheet(self): self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() + def on_reader_change(self, n): + self.select_reader(n) + self.load_data() + def select_reader(self, n): if self.source != self.LOCAL_FILE: return # ignore for URL's @@ -335,14 +342,11 @@ def select_reader(self, n): path = self.recent_paths[0] if n == 0: # default path.file_format = None - self.load_data() elif n <= len(self.available_readers): reader = self.available_readers[n - 1] path.file_format = reader.qualified_name() - self.load_data() else: # the rest include just qualified names path.file_format = self.reader_combo.itemText(n) - self.load_data() def _url_set(self): index = self.url_combo.currentIndex() @@ -373,7 +377,9 @@ def browse_file(self, in_demos=False): else: start_file = self.last_path() or os.path.expanduser("~/") - filename, reader, _ = open_filename_dialog(start_file, None, self.available_readers) + filename, reader, _ = open_filename_dialog( + start_file, None, self.available_readers, + add_all="*") if not filename: return self.add_path(filename) @@ -415,20 +421,20 @@ def _try_load(self): if not url: return self.Information.no_file_selected - def mark_problematic_reader(): - self.reader_combo.setItemData(self.reader_combo.currentIndex(), - QBrush(Qt.red), Qt.ForegroundRole) - try: self.reader = self._get_reader() # also sets current reader index assert self.reader is not None except MissingReaderException: - mark_problematic_reader() - return self.Error.missing_reader + if self.reader_combo.currentIndex() > 0: + return self.Error.missing_reader + else: + return self.Error.select_file_type except Exception as ex: - mark_problematic_reader() log.exception(ex) - return lambda x=ex: self.Error.unknown(str(x)) + if self.reader_combo.currentIndex() > 0: + return lambda x=ex: self.Error.unknown(str(x)) + else: + return lambda x=ex: self.Error.unknown_select(str(x)) try: self._update_sheet_combo() @@ -439,7 +445,6 @@ def mark_problematic_reader(): try: data = self.reader.read() except Exception as ex: - mark_problematic_reader() log.exception(ex) return lambda x=ex: self.Error.unknown(str(x)) if warnings: @@ -455,9 +460,25 @@ def mark_problematic_reader(): return None def _get_reader(self) -> FileFormat: + """ + Get the reader for the current file. + + For local files, this also observes the stored settings and the reader + combo, as follows: + + 1. If the file format is known (from stored settings), use it and set + the reader combo to the corresponding index (as in settings) + 2. Otherwise, detect it from the extension and set the combo to + Auto detect, overriding any previous user-set choice + 3. Otherwise, use the current combo state. + + Returns: + FileFormat: reader instance + """ if self.source == self.LOCAL_FILE: path = self.last_path() self.reader_combo.setEnabled(True) + if self.recent_paths and self.recent_paths[0].file_format: qname = self.recent_paths[0].file_format qname_index = {r.qualified_name(): i for i, r in enumerate(self.available_readers)} @@ -473,9 +494,20 @@ def _get_reader(self) -> FileFormat: except Exception as ex: raise MissingReaderException(f'Can not find reader "{qname}"') from ex reader = reader_class(path) + else: - self.reader_combo.setCurrentIndex(0) - reader = FileFormat.get_reader(path) + old_idx = self.reader_combo.currentIndex() + try: + self.reader_combo.setCurrentIndex(0) + reader = FileFormat.get_reader(path) + except MissingReaderException: + if old_idx == 0: + raise + # Set the path for the current file format, + # and repeat the call to return the corresponding reader + self.select_reader(old_idx) + return self._get_reader() + if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader @@ -504,12 +536,21 @@ def _select_active_sheet(self): self.sheet_combo.setCurrentIndex(0) def _initialize_reader_combo(self): - self.reader_combo.clear() - filters = [format_filter(f) for f in self.available_readers] - self.reader_combo.addItems([DEFAULT_READER_TEXT] + filters) - self.reader_combo.setCurrentIndex(0) - self.reader_combo.setDisabled(True) - # additional readers may be added in self._get_reader() + # Reset to initial state without losing the current index or + # emitting any signals. + combo = self.reader_combo + if not combo.count(): + filters = [format_filter(f) for f in self.available_readers] + combo.addItems([DEFAULT_READER_TEXT] + filters) + combo.setCurrentIndex(0) + else: + # additional readers may be added in self._get_reader() + n = len(self.available_readers) + 1 + if combo.currentIndex() >= n: + combo.setCurrentIndex(0) + while combo.count() > n: + combo.removeItem(combo.count() - 1) + combo.setDisabled(True) @staticmethod def _describe(table): diff --git a/Orange/widgets/data/tests/actually-a-tab-file.xlsx b/Orange/widgets/data/tests/actually-a-tab-file.xlsx new file mode 100644 index 00000000000..87dffb64ded --- /dev/null +++ b/Orange/widgets/data/tests/actually-a-tab-file.xlsx @@ -0,0 +1,27 @@ +age prescription astigmatic tear_rate lenses +discrete discrete discrete discrete discrete + class +young myope no reduced none +young myope no normal soft +young myope yes reduced none +young myope yes normal hard +young hypermetrope no reduced none +young hypermetrope no normal soft +young hypermetrope yes reduced none +young hypermetrope yes normal hard +pre-presbyopic myope no reduced none +pre-presbyopic myope no normal soft +pre-presbyopic myope yes reduced none +pre-presbyopic myope yes normal hard +pre-presbyopic hypermetrope no reduced none +pre-presbyopic hypermetrope no normal soft +pre-presbyopic hypermetrope yes reduced none +pre-presbyopic hypermetrope yes normal none +presbyopic myope no reduced none +presbyopic myope no normal none +presbyopic myope yes reduced none +presbyopic myope yes normal hard +presbyopic hypermetrope no reduced none +presbyopic hypermetrope no normal soft +presbyopic hypermetrope yes reduced none +presbyopic hypermetrope yes normal none diff --git a/Orange/widgets/data/tests/an_excel_file-too.foo b/Orange/widgets/data/tests/an_excel_file-too.foo new file mode 100644 index 0000000000000000000000000000000000000000..bc3fd8089b5f829bc2a8ec89b23690072893a515 GIT binary patch literal 8932 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 Z-_AuP5E^240RT+I#~Y#E3ba3d{U1~!hp+$u literal 0 HcmV?d00001 diff --git a/Orange/widgets/data/tests/an_excel_file.foo b/Orange/widgets/data/tests/an_excel_file.foo new file mode 100644 index 0000000000000000000000000000000000000000..bc3fd8089b5f829bc2a8ec89b23690072893a515 GIT binary patch literal 8932 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 Z-_AuP5E^240RT+I#~Y#E3ba3d{U1~!hp+$u literal 0 HcmV?d00001 diff --git a/Orange/widgets/data/tests/an_excel_file.xlsx b/Orange/widgets/data/tests/an_excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bc3fd8089b5f829bc2a8ec89b23690072893a515 GIT binary patch literal 8932 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 Z-_AuP5E^240RT+I#~Y#E3ba3d{U1~!hp+$u literal 0 HcmV?d00001 diff --git a/Orange/widgets/data/tests/test_owfile.py b/Orange/widgets/data/tests/test_owfile.py index 27ef58e747b..b5e55d17607 100644 --- a/Orange/widgets/data/tests/test_owfile.py +++ b/Orange/widgets/data/tests/test_owfile.py @@ -23,7 +23,7 @@ Domain, DiscreteVariable, ContinuousVariable from Orange.util import OrangeDeprecationWarning -from Orange.data.io import TabReader +from Orange.data.io import TabReader, XlsReader from Orange.tests import named_file from Orange.widgets.data.owfile import OWFile, OWFileDropHandler, DEFAULT_READER_TEXT from Orange.widgets.utils.filedialogs import dialog_formats, format_filter, RecentPath @@ -361,13 +361,13 @@ def test_reader_custom_tab(self): outdata = self.get_output(self.widget.Outputs.data) self.assertEqual(len(outdata), 150) # loaded iris - def test_no_reader_extension(self): + def test_unknown_extension(self): with named_file("", suffix=".xyz_unknown") as fn: no_reader = RecentPath(fn, None, None) self.widget = self.create_widget(OWFile, stored_settings={"recent_paths": [no_reader]}) self.widget.load_data() - self.assertTrue(self.widget.Error.missing_reader.is_shown()) + self.assertTrue(self.widget.Error.select_file_type.is_shown()) def test_fail_sheets(self): with named_file("", suffix=".failed_sheet") as fn: @@ -418,6 +418,22 @@ def test_no_specified_reader(self): self.assertTrue(self.widget.Error.missing_reader.is_shown()) self.assertEqual(self.widget.reader_combo.currentText(), "not.a.file.reader.class") + + def _select_reader(self, name): + reader_combo = self.widget.reader_combo + len_with_qname = len(reader_combo) + for i in range(len_with_qname): + text = reader_combo.itemText(i) + if text.startswith(name): + break + else: + assert f"No reader starts with {name!r}" + reader_combo.setCurrentIndex(i) + reader_combo.activated.emit(i) + + def _select_tab_reader(self): + self._select_reader("Tab-separated") + def test_select_reader(self): filename = FileFormat.locate("iris.tab", dataset_dirs) @@ -436,12 +452,7 @@ def test_select_reader(self): self.assertEqual(self.widget.reader_combo.currentText(), "not.a.file.reader.class") self.assertEqual(self.widget.reader, None) - # select the tab reader - for i in range(len_with_qname): - text = self.widget.reader_combo.itemText(i) - if text.startswith("Tab-separated"): - break - self.widget.reader_combo.activated.emit(i) + self._select_tab_reader() self.assertEqual(len(self.widget.reader_combo), len_with_qname - 1) self.assertTrue(self.widget.reader_combo.currentText().startswith("Tab-separated")) self.assertIsInstance(self.widget.reader, TabReader) @@ -452,6 +463,105 @@ def test_select_reader(self): self.assertEqual(self.widget.reader_combo.currentText(), DEFAULT_READER_TEXT) self.assertIsInstance(self.widget.reader, TabReader) + def test_auto_detect_and_override(self): + tab_as_xlsx = FileFormat.locate("actually-a-tab-file.xlsx", dataset_dirs) + iris = FileFormat.locate("iris", dataset_dirs) + + reader_combo = self.widget.reader_combo + + reader_combo.setCurrentIndex(0) + reader_combo.activated.emit(0) + assert (self.widget.reader_combo.currentText() + == "Determine type from the file extension") + + def open_file(_a, _b, _c, filters, _e): + return filename, filters.split(";;")[0] + + with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName", + open_file): + + # Loading a tab file with extension xlsx fails with auto-detect + filename = tab_as_xlsx + self.widget.browse_file() + + self.assertEqual(self.widget.reader_combo.currentText(), + "Determine type from the file extension") + self.assertTrue(self.widget.Error.unknown_select.is_shown()) + self.assertIsNone(self.get_output(self.widget.Outputs.data)) + + # Select the tab reader: it should work + self._select_tab_reader() + assert "Tab-separated" in self.widget.reader_combo.currentText() + + self.assertFalse(self.widget.Error.unknown_select.is_shown()) + self.assertIsInstance(self.widget.reader, TabReader) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Switching to iris resets the combo to auto-detect + filename = iris + self.widget.browse_file() + self.assertEqual(self.widget.reader_combo.currentText(), + "Determine type from the file extension") + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Taking the tab-as-xlsx file from recent paths should restore + # the file type for that file + self.widget.file_combo.setCurrentIndex(1) + self.widget.file_combo.activated.emit(1) + self.assertIn("Tab-separated", self.widget.reader_combo.currentText()) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Reloading should work + self.widget.load_data() + self.assertIn("Tab-separated", self.widget.reader_combo.currentText()) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Loading this file - not from history - should fail + filename = tab_as_xlsx + self.widget.browse_file() + self.assertTrue(self.widget.Error.unknown_select.is_shown()) + self.assertIsNone(self.get_output(self.widget.Outputs.data)) + + # Set the correct type again (preparation for the next text block) + self._select_tab_reader() + assert not self.widget.Error.unknown_select.is_shown() + assert isinstance(self.widget.reader, TabReader) + assert self.get_output(self.widget.Outputs.data) is not None + + # Now load a real Excel file: this is a known excention so the combo + # should return to auto-detect + filename = FileFormat.locate("an_excel_file.xlsx", dataset_dirs) + self.widget.browse_file() + self.assertEqual(self.widget.reader_combo.currentText(), + "Determine type from the file extension") + self.assertFalse(self.widget.Error.unknown_select.is_shown()) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Load iris to prepare for the next test block + filename = iris + self.widget.browse_file() + assert (self.widget.reader_combo.currentText() + == "Determine type from the file extension") + assert self.get_output(self.widget.Outputs.data) is not None + + # Files with unknown extensions require manual selection + filename = FileFormat.locate("an_excel_file.foo", dataset_dirs) + self.widget.browse_file() + self.assertTrue(self.widget.Error.select_file_type.is_shown()) + self.assertIsNone(self.get_output(self.widget.Outputs.data)) + + self._select_reader("Excel") + self.assertFalse(self.widget.Error.unknown_select.is_shown()) + self.assertFalse(self.widget.Error.select_file_type.is_shown()) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + # Consecutive loading of files with the same extension keeps selection + filename = FileFormat.locate("an_excel_file-too.foo", dataset_dirs) + self.widget.browse_file() + self.assertFalse(self.widget.Error.unknown_select.is_shown()) + self.assertFalse(self.widget.Error.select_file_type.is_shown()) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + def test_select_reader_errors(self): filename = FileFormat.locate("iris.tab", dataset_dirs) diff --git a/i18n/si/msgs.jaml b/i18n/si/msgs.jaml index c082112ff35..ef64b875a14 100644 --- a/i18n/si/msgs.jaml +++ b/i18n/si/msgs.jaml @@ -6161,7 +6161,7 @@ widgets/data/owfeaturestatistics.py: __main__: false iris: false widgets/data/owfile.py: - Automatically detect type: Samodejno zaznaj vrsto datoteke + Determine type from the file extension: Določi vrsto iz končnice datoteke def `add_origin`: type: false origin: false @@ -6196,8 +6196,10 @@ widgets/data/owfile.py: class `Error`: File not found.: Datoteka ni najdena. Missing reader.: Bralnik za ta tip ne obstaja. + Select file type.: Izberite vrsto datoteke. Error listing available sheets.: Napaka ob ustvarjanju seznama listov. Read error:\n{}: Napaka ob branju:\n{} + Read error, possibly due to incorrect choice of file type:\n{}: Napaka ob branju, morda zaradi napačne izbire vrste datoteke:\n{} 'Use CSV File Import widget for advanced options ': 'Uporabi Bralnik CSV za napredne možnosti ' for comma-separated files: za datoteke ločene z vejico use-csv-file-import: falxe @@ -6232,10 +6234,11 @@ widgets/data/owfile.py: File: Datoteka Cannot find the directory with documentation datasets: Ne najdem mape s podatki iz dokumentacije ~/: false + *: false def `load_data`: No data.: Ni podatkov. def `_get_reader`: - Can not find reader "{qname}": Bralnik "{qname}" ne obstaja. + Can not find reader "{qname}": Ne najdem bralnika "{qname}" def `_describe`: attributes: false Name: Ime From 6c00ce157a14a4ec3b8613d6f1a476c81dddc1c1 Mon Sep 17 00:00:00 2001 From: janezd Date: Sun, 19 Jan 2025 11:55:01 +0100 Subject: [PATCH 2/2] pylint --- Orange/widgets/data/owfile.py | 9 +++++++-- Orange/widgets/data/tests/test_owfile.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Orange/widgets/data/owfile.py b/Orange/widgets/data/owfile.py index 227df179301..f6b18c8b086 100644 --- a/Orange/widgets/data/owfile.py +++ b/Orange/widgets/data/owfile.py @@ -9,7 +9,6 @@ QStyle, QComboBox, QMessageBox, QGridLayout, QLabel, \ QLineEdit, QSizePolicy as Policy, QCompleter from AnyQt.QtCore import Qt, QTimer, QSize, QUrl -from AnyQt.QtGui import QBrush from orangewidget.utils.filedialogs import format_filter from orangewidget.workflow.drophandler import SingleUrlDropHandler @@ -327,6 +326,7 @@ def select_file(self, n): self.set_file_list() def select_sheet(self): + # pylint: disable=unsubscriptable-object self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() @@ -339,7 +339,7 @@ def select_reader(self, n): return # ignore for URL's if self.recent_paths: - path = self.recent_paths[0] + path = self.recent_paths[0] # pylint: disable=unsubscriptable-object if n == 0: # default path.file_format = None elif n <= len(self.available_readers): @@ -384,6 +384,7 @@ def browse_file(self, in_demos=False): return self.add_path(filename) if reader is not None: + # pylint: disable=unsubscriptable-object self.recent_paths[0].file_format = reader.qualified_name() self.source = self.LOCAL_FILE @@ -479,6 +480,7 @@ def _get_reader(self) -> FileFormat: path = self.last_path() self.reader_combo.setEnabled(True) + # pylint: disable=unsubscriptable-object if self.recent_paths and self.recent_paths[0].file_format: qname = self.recent_paths[0].file_format qname_index = {r.qualified_name(): i for i, r in enumerate(self.available_readers)} @@ -508,6 +510,7 @@ def _get_reader(self) -> FileFormat: self.select_reader(old_idx) return self._get_reader() + # pylint: disable=unsubscriptable-object if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader @@ -597,10 +600,12 @@ def _describe(table): return text def storeSpecificSettings(self): + # pylint: disable=unsubscriptable-object self.current_context.modified_variables = self.variables[:] def retrieveSpecificSettings(self): if hasattr(self.current_context, "modified_variables"): + # pylint: disable=unsubscriptable-object self.variables[:] = self.current_context.modified_variables def reset_domain_edit(self): diff --git a/Orange/widgets/data/tests/test_owfile.py b/Orange/widgets/data/tests/test_owfile.py index b5e55d17607..c414e1f9887 100644 --- a/Orange/widgets/data/tests/test_owfile.py +++ b/Orange/widgets/data/tests/test_owfile.py @@ -1,5 +1,5 @@ # Test methods with long descriptive names can omit docstrings -# pylint: disable=missing-docstring,protected-access +# pylint: disable=missing-docstring,protected-access,too-many-public-methods from os import path, remove, getcwd from os.path import dirname import unittest @@ -23,7 +23,7 @@ Domain, DiscreteVariable, ContinuousVariable from Orange.util import OrangeDeprecationWarning -from Orange.data.io import TabReader, XlsReader +from Orange.data.io import TabReader from Orange.tests import named_file from Orange.widgets.data.owfile import OWFile, OWFileDropHandler, DEFAULT_READER_TEXT from Orange.widgets.utils.filedialogs import dialog_formats, format_filter, RecentPath @@ -41,7 +41,9 @@ class FailedSheetsFormat(FileFormat): def read(self): pass + @property def sheets(self): + # pylint: disable=broad-exception-raised raise Exception("Not working") @@ -137,6 +139,7 @@ def _drop_event(self, url): def test_check_file_size(self): self.assertFalse(self.widget.Warning.file_too_big.is_shown()) self.widget.SIZE_LIMIT = 4000 + # We're avoiding __new__, pylint: disable=unnecessary-dunder-call self.widget.__init__() self.assertTrue(self.widget.Warning.file_too_big.is_shown()) @@ -337,7 +340,9 @@ def test_check_datetime_disabled(self): with named_file(dat, suffix=".tab") as filename: self.open_dataset(filename) domain_editor = self.widget.domain_editor - idx = lambda x: self.widget.domain_editor.model().createIndex(x, 1) + + def idx(x): + return self.widget.domain_editor.model().createIndex(x, 1) qcombobox = QComboBox() combo = ComboDelegate(domain_editor,