Skip to content

Commit

Permalink
Merge pull request #1608 from janezd/domain-model
Browse files Browse the repository at this point in the history
Implement DomainModel
  • Loading branch information
kernc authored Oct 17, 2016
2 parents 20e7e66 + 4c8a628 commit 4d1ea03
Show file tree
Hide file tree
Showing 14 changed files with 522 additions and 760 deletions.
102 changes: 66 additions & 36 deletions Orange/widgets/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1526,14 +1526,12 @@ def minimumSizeHint(self):


# TODO comboBox looks overly complicated:
# - is the argument control2attributeDict needed? doesn't emptyString do the
# job?
# - can valueType be anything else than str?
# - sendSelectedValue is not a great name
def comboBox(widget, master, value, box=None, label=None, labelWidth=None,
orientation=Qt.Vertical, items=(), callback=None,
sendSelectedValue=False, valueType=str,
control2attributeDict=None, emptyString=None, editable=False,
emptyString=None, editable=False,
contentsLength=None, maximumContentsLength=25,
**misc):
"""
Expand All @@ -1543,9 +1541,6 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None,
selected row (if `sendSelected` is left at default, `False`) or a value
converted to `valueType` (`str` by default).
Furthermore, the value is converted by looking up into dictionary
`control2attributeDict`.
:param widget: the widget into which the box is inserted
:type widget: PyQt4.QtGui.QWidget or None
:param master: master widget
Expand All @@ -1571,9 +1566,6 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None,
:param valueType: the type into which the selected value is converted
if sentSelectedValue is `False`
:type valueType: type
:param control2attributeDict: a dictionary through which the value is
converted
:type control2attributeDict: dict or None
:param emptyString: the string value in the combo box that gets stored as
an empty string in `value`
:type emptyString: str
Expand All @@ -1590,6 +1582,10 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None,
length (default: 25, use 0 to disable)
:rtype: PyQt4.QtGui.QComboBox
"""

# Local import to avoid circular imports
from Orange.widgets.utils.itemmodels import VariableListModel

if box or label:
hb = widgetBox(widget, box, orientation, addToLayout=False)
if label is not None:
Expand All @@ -1615,29 +1611,35 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None,

if value:
cindex = getdeepattr(master, value)
if isinstance(cindex, str):
if items and cindex in items:
cindex = items.index(getdeepattr(master, value))
else:
model = misc.get("model", None)
if isinstance(model, VariableListModel):
callfront = CallFrontComboBoxModel(combo, model)
callfront.action(cindex)
else:
if isinstance(cindex, str):
if items and cindex in items:
cindex = items.index(cindex)
else:
cindex = 0
if cindex > combo.count() - 1:
cindex = 0
if cindex > combo.count() - 1:
cindex = 0
combo.setCurrentIndex(cindex)

if sendSelectedValue:
if control2attributeDict is None:
control2attributeDict = {}
if emptyString:
control2attributeDict[emptyString] = ""
combo.setCurrentIndex(cindex)

if isinstance(model, VariableListModel):
connectControl(
master, value, callback, combo.activated[int],
callfront,
ValueCallbackComboModel(master, value, model)
)
elif sendSelectedValue:
connectControl(
master, value, callback, combo.activated[str],
CallFrontComboBox(combo, valueType, control2attributeDict),
ValueCallbackCombo(master, value, valueType,
control2attributeDict))
CallFrontComboBox(combo, valueType, emptyString),
ValueCallbackCombo(master, value, valueType, emptyString))
else:
connectControl(
master, value, callback, combo.activated[int],
CallFrontComboBox(combo, None, control2attributeDict))
CallFrontComboBox(combo, None, emptyString))
miscellanea(combo, hb, widget, **misc)
combo.emptyString = emptyString
return combo
Expand Down Expand Up @@ -2254,13 +2256,23 @@ def __call__(self, value):


class ValueCallbackCombo(ValueCallback):
def __init__(self, widget, attribute, f=None, control2attributeDict=None):
def __init__(self, widget, attribute, f=None, emptyString=""):
super().__init__(widget, attribute, f)
self.control2attributeDict = control2attributeDict or {}
self.emptyString = emptyString

def __call__(self, value):
value = str(value)
return super().__call__(self.control2attributeDict.get(value, value))
return super().__call__("" if value == self.emptyString else value)


class ValueCallbackComboModel(ValueCallback):
def __init__(self, widget, attribute, model):
super().__init__(widget, attribute)
self.model = model

def __call__(self, index):
# Can't use super here since, it doesn't set `None`'s?!
return self.acyclic_setattr(self.model[index])


class ValueCallbackLineEdit(ControlledCallback):
Expand Down Expand Up @@ -2434,18 +2446,15 @@ def action(self, value):


class CallFrontComboBox(ControlledCallFront):
def __init__(self, control, valType=None, control2attributeDict=None):
def __init__(self, control, valType=None, emptyString=""):
super().__init__(control)
self.valType = valType
if control2attributeDict is None:
self.attribute2controlDict = {}
else:
self.attribute2controlDict = \
{y: x for x, y in control2attributeDict.items()}
self.emptyString = emptyString

def action(self, value):
if value is not None:
value = self.attribute2controlDict.get(value, value)
if value == "":
value = self.emptyString
if self.valType:
for i in range(self.control.count()):
if self.valType(str(self.control.itemText(i))) == value:
Expand All @@ -2462,6 +2471,27 @@ def action(self, value):
self.control.setCurrentIndex(value)


class CallFrontComboBoxModel(ControlledCallFront):
def __init__(self, control, model):
super().__init__(control)
self.model = model

def action(self, value):
if value == "": # the latter accomodates PyListModel
value = None
if value is None and None not in self.model:
return # e.g. attribute x in uninitialized scatter plot
if value in self.model:
self.control.setCurrentIndex(self.model.indexOf(value))
return
elif isinstance(value, str):
for i, val in enumerate(self.model):
if value == str(val):
self.control.setCurrentIndex(i)
return
raise ValueError("Combo box does not contain item " + repr(value))


class CallFrontHSlider(ControlledCallFront):
def action(self, value):
if value is not None:
Expand Down
75 changes: 74 additions & 1 deletion Orange/widgets/tests/test_itemmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from unittest import TestCase

from PyQt4.QtCore import Qt
from Orange.widgets.utils.itemmodels import PyTableModel, PyListModel

from Orange.data import Domain, ContinuousVariable
from Orange.widgets.utils.itemmodels import \
PyTableModel, PyListModel, DomainModel


class TestPyTableModel(TestCase):
Expand Down Expand Up @@ -183,3 +186,73 @@ def test_insert_delete_rows(self):
success = model.removeRows(3, 4)
self.assertIs(success, True)
self.assertSequenceEqual(model, [None, None, None])


class TestDomainModel(TestCase):
def test_init_with_single_section(self):
model = DomainModel(order=DomainModel.CLASSES)
self.assertEqual(model.order, (DomainModel.CLASSES, ))

def test_separators(self):
attrs = [ContinuousVariable(n) for n in "abg"]
classes = [ContinuousVariable(n) for n in "deh"]
metas = [ContinuousVariable(n) for n in "ijf"]

model = DomainModel()
sep = [model.Separator]
model.set_domain(Domain(attrs, classes, metas))
self.assertEqual(list(model), classes + sep + metas + sep + attrs)

model = DomainModel()
model.set_domain(Domain(attrs, [], metas))
self.assertEqual(list(model), metas + sep + attrs)

model = DomainModel()
model.set_domain(Domain([], [], metas))
self.assertEqual(list(model), metas)

model = DomainModel(placeholder="foo")
model.set_domain(Domain([], [], metas))
self.assertEqual(list(model), [None] + sep + metas)

model = DomainModel(placeholder="foo")
model.set_domain(Domain(attrs, [], metas))
self.assertEqual(list(model), [None] + sep + metas + sep + attrs)

def test_placeholder_placement(self):
model = DomainModel(placeholder="foo")
sep = model.Separator
self.assertEqual(model.order, (None, sep) + model.SEPARATED)

model = DomainModel(order=("bar", ), placeholder="foo")
self.assertEqual(model.order, (None, "bar"))

model = DomainModel(order=("bar", None, "baz"), placeholder="foo")
self.assertEqual(model.order, ("bar", None, "baz"))

model = DomainModel(order=("bar", sep, "baz"),
placeholder="foo")
self.assertEqual(model.order, (None, sep, "bar", sep, "baz"))

def test_subparts(self):
attrs = [ContinuousVariable(n) for n in "abg"]
classes = [ContinuousVariable(n) for n in "deh"]
metas = [ContinuousVariable(n) for n in "ijf"]

m = DomainModel
sep = m.Separator
model = DomainModel(
order=(m.ATTRIBUTES | m.METAS, sep, m.CLASSES))
model.set_domain(Domain(attrs, classes, metas))
self.assertEqual(list(model), attrs + metas + [sep] + classes)

m = DomainModel
sep = m.Separator
model = DomainModel(
order=(m.ATTRIBUTES | m.METAS, sep, m.CLASSES),
alphabetical=True)
model.set_domain(Domain(attrs, classes, metas))
self.assertEqual(list(model),
sorted(attrs + metas, key=lambda x: x.name) +
[sep] +
sorted(classes, key=lambda x: x.name))
22 changes: 22 additions & 0 deletions Orange/widgets/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,25 @@ def to_html(str):
replace("<", "&#60;").replace(">", "&#62;").replace("=\\=", "&#8800;")

getHtmlCompatibleString = to_html


def checksum(x):
if x is None:
return None
try:
return x.checksum()
except:
return float('nan')


def get_variable_values_sorted(variable):
"""
Return a list of sorted values for given attribute, if all its values can be
cast to int's.
"""
if variable.is_continuous:
return []
try:
return sorted(variable.values, key=int)
except ValueError:
return variable.values
23 changes: 10 additions & 13 deletions Orange/widgets/utils/datacaching.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
from collections import defaultdict
from operator import itemgetter

def getCached(data, funct, params=(), kwparams=None):
def getCached(data, funct, params=(), **kwparams):
# pylint: disable=protected-access
if data is None:
return None
info = getattr(data, "__data_cache", None)
if info is not None:
if funct in data.info:
return data.info(funct)
else:
info = data.info = {}
if not hasattr(data, "__data_cache"):
data.__data_cache = {}
info = data.__data_cache
if funct in info:
return info[funct]
if isinstance(funct, str):
return None
if kwparams is None:
kwparams = {}
info[funct] = res = funct(*params, **kwparams)
return res


def setCached(data, name, value):
if data is None:
return
info = getattr(data, "__data_cache", None)
if info is None:
info = data.info = {}
info[name] = value
if not hasattr(data, "__data_cache"):
data.__data_cache = {}
data.__data_cache[name] = value

def delCached(data, name):
info = data is not None and getattr(data, "__data_cache")
Expand Down
Loading

0 comments on commit 4d1ea03

Please sign in to comment.