Skip to content

Commit

Permalink
OWFile: Annotate settings
Browse files Browse the repository at this point in the history
  • Loading branch information
janezd committed Mar 12, 2021
1 parent 40f34de commit dff6de6
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 53 deletions.
35 changes: 15 additions & 20 deletions Orange/widgets/data/owfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -14,22 +14,18 @@
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, \
open_filename_dialog
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__)


Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -471,13 +466,6 @@ def missing_prop(prop):
f"Last entry: {table[-1, 'Timestamp']}</p>"
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()
Expand Down Expand Up @@ -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()
11 changes: 10 additions & 1 deletion Orange/widgets/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
58 changes: 33 additions & 25 deletions Orange/widgets/utils/domaineditor.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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__()
Expand Down Expand Up @@ -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),
Expand All @@ -309,26 +316,27 @@ 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],
[(mt, Place.meta) for mt in domain.metas])):
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)

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))
Expand All @@ -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 ""
Expand All @@ -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)])
Expand Down Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions Orange/widgets/utils/tests/test_domaineditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand All @@ -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"])
Expand Down

0 comments on commit dff6de6

Please sign in to comment.