diff --git a/Orange/widgets/data/owfile.py b/Orange/widgets/data/owfile.py index 31f6ee147eb..fa4ec02b7dc 100644 --- a/Orange/widgets/data/owfile.py +++ b/Orange/widgets/data/owfile.py @@ -2,7 +2,7 @@ import logging from itertools import chain from urllib.parse import urlparse -from typing import List +from typing import List, Dict import numpy as np from AnyQt.QtWidgets import \ @@ -14,8 +14,9 @@ from Orange.data.io import FileFormat, UrlReader, class_from_qualified_name from Orange.util import log_warnings from Orange.widgets import widget, gui -from Orange.widgets.settings import Setting, ContextSetting, \ - PerfectDomainContextHandler, SettingProvider +from Orange.widgets.settings import \ + Setting, PerfectDomainContextHandler, SettingProvider +from Orange.widgets.utils import vartype from Orange.widgets.utils.domaineditor import DomainEditor from Orange.widgets.utils.itemmodels import PyListModel from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, \ @@ -23,13 +24,8 @@ from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.utils.state_summary import format_summary_details from Orange.widgets.widget import Output, Msg - -# Backward compatibility: class RecentPath used to be defined in this module, -# and it is used in saved (pickled) settings. It must be imported into the -# module's namespace so that old saved settings still work from Orange.widgets.utils.filedialogs import RecentPath - log = logging.getLogger(__name__) @@ -95,10 +91,11 @@ class Outputs: LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler( - match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL - ) + match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL) # pylint seems to want declarations separated from definitions + # This must be in the widget, not the mixin - otherwise all widgets will + # share the same paths recent_paths: List[RecentPath] recent_urls: List[str] variables: list @@ -114,11 +111,9 @@ class Outputs: ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) - sheet_names = Setting({}) + sheet_names: Dict[str, str] = Setting({}) url = Setting("") - variables = ContextSetting([]) - domain_editor = SettingProvider(DomainEditor) class Warning(widget.OWWidget.Warning): @@ -471,13 +466,6 @@ def missing_prop(prop): f"Last entry: {table[-1, 'Timestamp']}

" return text - def storeSpecificSettings(self): - self.current_context.modified_variables = self.variables[:] - - def retrieveSpecificSettings(self): - if hasattr(self.current_context, "modified_variables"): - self.variables[:] = self.current_context.modified_variables - def reset_domain_edit(self): self.domain_editor.reset_domain() self.apply_domain_edit() @@ -578,6 +566,13 @@ def workflowEnvChanged(self, key, value, oldvalue): """ self.update_file_list(key, value, oldvalue) + @classmethod + def migrate_context(cls, context, _): + if hasattr(context, "modified_variables"): + delattr(context, "modified_variables") + de_vars = context.values["domain_editor"]["variables"] + de_vars[:] = [(x[0], vartype(x[1]), *x[2:]) for x in de_vars] + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWFile).run() diff --git a/Orange/widgets/utils/__init__.py b/Orange/widgets/utils/__init__.py index e974de1cc2f..0a39c8e10e5 100644 --- a/Orange/widgets/utils/__init__.py +++ b/Orange/widgets/utils/__init__.py @@ -9,11 +9,20 @@ from AnyQt.QtCore import QObject -from Orange.data.variable import TimeVariable +from Orange.data.variable import \ + Variable, DiscreteVariable, ContinuousVariable, StringVariable, TimeVariable from Orange.util import deepgetattr +_vartype2int = {DiscreteVariable: 1, + ContinuousVariable: 2, + StringVariable: 3, + TimeVariable: 4} + def vartype(var): + if isinstance(var, type) and issubclass(var, Variable): + return _vartype2int[var] + if var.is_discrete: return 1 elif var.is_continuous: diff --git a/Orange/widgets/utils/domaineditor.py b/Orange/widgets/utils/domaineditor.py index d9f9b016540..9247639d687 100644 --- a/Orange/widgets/utils/domaineditor.py +++ b/Orange/widgets/utils/domaineditor.py @@ -1,5 +1,6 @@ from itertools import chain from copy import deepcopy +from typing import List, Tuple import numpy as np import scipy.sparse as sp @@ -16,6 +17,7 @@ from Orange.widgets import gui from Orange.widgets.gui import HorizontalGridDelegate from Orange.widgets.settings import ContextSetting +from Orange.widgets.utils import vartype from Orange.widgets.utils.itemmodels import TableModel @@ -40,8 +42,9 @@ class VarTableModel(QAbstractTableModel): places = "feature", "target", "meta", "skip" typenames = "categorical", "numeric", "text", "datetime" vartypes = DiscreteVariable, ContinuousVariable, StringVariable, TimeVariable - name2type = dict(zip(typenames, vartypes)) - type2name = dict(zip(vartypes, typenames)) + vtype2name = {vartype(vt): name for vt, name in zip(vartypes, typenames)} + name2vtype = {v: k for k, v in vtype2name.items()} + vtype2type = {vartype(vt): vt for vt in vartypes} def __init__(self, variables, *args): super().__init__(*args) @@ -72,14 +75,14 @@ def data(self, index, role): val = self.variables[row][col] if role in (Qt.DisplayRole, Qt.EditRole): if col == Column.tpe: - return self.type2name[val] + return self.vtype2name[val] if col == Column.place: return self.places[val] else: return val if role == Qt.DecorationRole: if col == Column.tpe: - return gui.attributeIconDict[self.vartypes.index(val) + 1] + return gui.attributeIconDict[val] if role == Qt.ForegroundRole: if self.variables[row][Column.place] == Place.skip \ and col != Column.place: @@ -96,19 +99,22 @@ def data(self, index, role): return None def setData(self, index, value, role=Qt.EditRole): + def set_at(c, value): + row_data = self.variables[row] + self.variables[row] = row_data[:c] + (value, ) + row_data[c + 1:] + row, col = index.row(), index.column() - row_data = self.variables[row] if role == Qt.EditRole: if col == Column.name and not (value.isspace() or value == ""): - row_data[col] = value + set_at(Column.name, value) elif col == Column.tpe: - vartype = self.name2type[value] - row_data[col] = vartype - if not vartype.is_primitive() and \ - row_data[Column.place] < Place.meta: - row_data[Column.place] = Place.meta + vtype = self.name2vtype[value] + set_at(Column.tpe, vtype) + if not self.vtype2type[vtype].is_primitive() and \ + self.variables[row][Column.place] < Place.meta: + set_at(Column.place, Place.meta) elif col == Column.place: - row_data[col] = self.places.index(value) + set_at(Column.place, self.places.index(value)) else: return False # Settings may change background colors @@ -185,8 +191,9 @@ def setEditorData(self, combo, index): class PlaceDelegate(ComboDelegate): def setEditorData(self, combo, index): combo.clear() - to_meta = not self.view.model().variables[ - index.row()][Column.tpe].is_primitive() + model = self.view.model() + vtype = model.variables[index.row()][Column.tpe] + to_meta = not model.vtype2type[vtype].is_primitive() combo.addItems(self.items[2 * to_meta:]) combo.setCurrentIndex(self.items.index(index.data()) - 2 * to_meta) combo.showPopup() @@ -200,7 +207,7 @@ class DomainEditor(QTableView): widget : parent widget """ - variables = ContextSetting([]) + variables: List[Tuple[str, int, int, str, bool]] = ContextSetting([]) def __init__(self, widget): super().__init__() @@ -285,14 +292,14 @@ def get_domain(self, domain, data, deduplicate=False): cols = [[], [], []] # Xcols, Ycols, Mcols def numbers_are_round(var, col_data): - if type(var) == ContinuousVariable: + if type(var) is ContinuousVariable: data = np.asarray(col_data.data) # Works for dense and sparse data = data[~np.isnan(data)] return (data == data.astype(int)).all() return False # Exit early with original domain if the user didn't actually change anything - if all((name == orig_var.name and tpe == type(orig_var) and place == orig_plc) + if all((name == orig_var.name and tpe == vartype(orig_var) and place == orig_plc) for (name, tpe, place, _, _), (orig_var, orig_plc) in zip(variables, chain(((at, Place.feature) for at in domain.attributes), @@ -309,7 +316,7 @@ def numbers_are_round(var, col_data): else: renamed_iter = iter(relevant_names) renamed = [] - for (name, tpe, place, _, may_be_numeric), (orig_var, orig_plc) in \ + for (name, vartpe, place, _, may_be_numeric), (orig_var, orig_plc) in \ zip(variables, chain([(at, Place.feature) for at in domain.attributes], [(cl, Place.class_var) for cl in domain.class_vars], @@ -317,6 +324,7 @@ def numbers_are_round(var, col_data): if place == Place.skip: continue + tpe = VarTableModel.vtype2type[vartpe] new_name = next(renamed_iter) if new_name != name and name not in renamed: renamed.append(name) @@ -324,11 +332,11 @@ def numbers_are_round(var, col_data): col_data = self._get_column(data, orig_var, orig_plc) is_sparse = sp.issparse(col_data) - if new_name == orig_var.name and tpe == type(orig_var): + if new_name == orig_var.name and tpe is type(orig_var): var = orig_var - elif tpe == type(orig_var): + elif tpe is type(orig_var): var = orig_var.copy(name=new_name) - elif tpe == DiscreteVariable: + elif tpe is DiscreteVariable: values = natural_sorted( list(str(i) for i in unique(col_data) if not self._is_missing(i)) @@ -340,7 +348,7 @@ def numbers_are_round(var, col_data): values = [str(int(float(v))) for v in values] var = tpe(new_name, values) col_data = self._to_column(col_data, is_sparse) - elif tpe == StringVariable: + elif tpe is StringVariable: var = tpe.make(new_name) if type(orig_var) in [DiscreteVariable, TimeVariable]: col_data = [orig_var.repr_val(x) if not np.isnan(x) else "" @@ -354,7 +362,7 @@ def numbers_are_round(var, col_data): # don't obey sparsity for StringVariable since they are # in metas which are transformed to dense below col_data = self._to_column(col_data, False, dtype=object) - elif tpe == ContinuousVariable and type(orig_var) == DiscreteVariable: + elif tpe is ContinuousVariable and type(orig_var) is DiscreteVariable: var = tpe.make(new_name) if may_be_numeric: col_data = [np.nan if self._is_missing(x) else float(orig_var.values[int(x)]) @@ -439,9 +447,9 @@ def discrete_value_display(value_list): return result return [ - [var.name, type(var), place, + (var.name, vartype(var), place, discrete_value_display(var.values) if var.is_discrete else "", - may_be_numeric(var)] + may_be_numeric(var)) for place, vars in enumerate( (domain.attributes, domain.class_vars, domain.metas)) for var in vars diff --git a/Orange/widgets/utils/tests/test_domaineditor.py b/Orange/widgets/utils/tests/test_domaineditor.py index 44e7bb42e31..b0d3bd1c829 100644 --- a/Orange/widgets/utils/tests/test_domaineditor.py +++ b/Orange/widgets/utils/tests/test_domaineditor.py @@ -23,13 +23,15 @@ def setUp(self): self.widget = MockWidget() self.editor = self.widget.domain_editor + discrete, continuous, string, time = range(1, 5) + self.orig_variables = [ - ["d1", DiscreteVariable, 0, "x, y, z, ...", False], - ["d2", DiscreteVariable, 0, "1, 2, 3, ...", True], - ["c1", ContinuousVariable, 0, "", True], - ["d3", DiscreteVariable, 1, "4, 3, 6, ...", True], - ["s", StringVariable, 2, "", False], - ["t", TimeVariable, 2, "", True] + ("d1", discrete, 0, "x, y, z, ...", False), + ("d2", discrete, 0, "1, 2, 3, ...", True), + ("c1", continuous, 0, "", True), + ("d3", discrete, 1, "4, 3, 6, ...", True), + ("s", string, 2, "", False), + ("t", time, 2, "", True) ] self.domain = Domain( [DiscreteVariable("d1", values=list("xyzw")), @@ -51,7 +53,7 @@ def test_deduplication(self): # No duplicates - domain, _ = \ + domain, *_ = \ editor.get_domain(self.domain, data) self.assertEqual([var.name for var in domain.attributes], ["d1", "d2", "c1"])