diff --git a/Orange/widgets/data/oweditdomain.py b/Orange/widgets/data/oweditdomain.py
index 02ea2b0784d..48fc3a944a0 100644
--- a/Orange/widgets/data/oweditdomain.py
+++ b/Orange/widgets/data/oweditdomain.py
@@ -6,8 +6,8 @@
"""
import sys
-
-from itertools import chain, zip_longest
+from xml.sax.saxutils import escape
+from itertools import zip_longest
from contextlib import contextmanager
from collections import namedtuple
from functools import singledispatch
@@ -1123,10 +1123,24 @@ def sizeHint(self):
return sh.expandedTo(QSize(660, 550))
def send_report(self):
+
if self.data is not None:
- self.report_raw("", EditDomainReport(
- old_domain=chain(self.data.domain.variables, self.data.domain.metas),
- new_domain=self.variables_model).to_html())
+ model = self.variables_model
+ state = ((model.data(midx, Qt.EditRole),
+ model.data(midx, TransformRole))
+ for i in range(model.rowCount())
+ for midx in [model.index(i)])
+ parts = []
+ for var, trs in state:
+ if trs:
+ parts.append(report_transform(var, trs))
+ if parts:
+ html = ("
" +
+ "".join(map("- {}
".format, parts)) +
+ "
")
+ else:
+ html = "No changes"
+ self.report_raw("", html)
else:
self.report_data(None)
@@ -1174,98 +1188,89 @@ def migrate_context(cls, context, version):
context.values["_domain_change_store"] = (dict(store), -2)
-
-class EditDomainReport:
- """Report creator for changes made in the OWEditDomain widget.
+def report_transform(var, trs):
+ # type: (Variable, List[Transform]) -> str
+ """
+ Return a html fragment summarizing the changes applied by `trs` list.
Parameters
----------
- old_domain : list of Variable
- input variables
- new_domain : list of Variable
- variables with applied changes
- """
-
- VARIABLE_HTML = "{}".format
- INDENTED_ITEM = "{}
".format
-
- def __init__(self, model):
- self.model = model
+ var : Variable
+ A variable descriptor no which trs operates
+ trs : List[Transform]
+ A non empty list of `Transform` instances.
- # self.old_domain = old_domain
- # self.new_domain = new_domain
+ Returns
+ -------
+ report : str
+ """
+ def strike(text):
+ return "{}".format(escape(text))
- def to_html(self):
- """Collect changes to variable names, values and labels
- and render them to html.
- """
- model = self.model
- index = model.index
- data = model.data
- for var, tr in ((data(i, Qt.EditRole), data(i, TransformRole))
- for i in range(model.rowCount()) for i in (index(i),)):
- changes = []
- rename = annotate = catmap = None
- for tr_ in tr:
- if isinstance(tr_, Rename):
- rename = tr_
- elif isinstance(tr_, Annotate):
- annotate = tr_
- elif isinstance(tr_, CategoriesMapping):
- catmap = tr_
- if rename is not None:
- changes.append("{} → {}".format(var.name, rename.name))
-
- all_changes = []
- for old_var, new_var in zip(self.old_domain, self.new_domain):
- changes = list(chain.from_iterable([
- self._section("Values", self._value_changes(old_var, new_var)),
- self._section("Labels", self._label_changes(old_var, new_var))
- ]))
-
- padding_top = ".5em" if all_changes else "0"
- if old_var.name != new_var.name:
- all_changes.append(self.VARIABLE_HTML(
- padding_top, "{} → {}".format(old_var.name, new_var.name)))
- elif changes:
- all_changes.append(self.VARIABLE_HTML(padding_top, old_var.name))
- all_changes.extend(changes)
- return "".format("".join(all_changes)) if all_changes else "No changes"
-
- def _section(self, name, changes):
- """Generator that adds section name if there were any changes made."""
- changes = list(changes)
- if changes:
- yield "{}:
".format(name)
- yield from changes
-
- def _value_changes(self, old_variable, new_variable):
- """Generator of all renamed values"""
- if not old_variable.is_discrete:
- return
- for old_value, new_value in zip(old_variable.values, new_variable.values):
- if old_value != new_value:
- yield self.INDENTED_ITEM("{} → {}".format(old_value, new_value))
+ def i(text):
+ return "{}".format(escape(text))
- def _label_changes(self, old_variable, new_variable):
- """Generator of all added, removed and modified labels"""
- old_labels = old_variable.attributes
- new_labels = new_variable.attributes
+ def text(text):
+ return "{}".format(escape(text))
+ assert trs
+ rename = annotate = catmap = None
- for name, value in new_labels.items():
- if name not in old_labels:
- yield self.INDENTED_ITEM("{}: {} (new)"
- .format(name, value))
+ for tr in trs:
+ if isinstance(tr, Rename):
+ rename = tr
+ elif isinstance(tr, Annotate):
+ annotate = tr
+ elif isinstance(tr, CategoriesMapping):
+ catmap = tr
+ if rename is not None:
+ header = "{} → {}".format(var.name, rename.name)
+ else:
+ header = var.name
+ values_section = None
+ if catmap is not None:
+ values_section = ("Values", [])
+ lines = values_section[1]
+ for ci, cj in catmap.mapping:
+ if ci is None:
+ item = cj + (" " * 3) + "(added)"
+ elif cj is None:
+ item = strike(ci)
+ else:
+ item = ci + " → " + cj
+ lines.append(item)
+
+ annotate_section = None
+ if annotate is not None:
+ annotate_section = ("Labels", [])
+ lines = annotate_section[1]
+ old = dict(var.annotations)
+ new = dict(annotate.annotations)
+ for name in sorted(set(old) - set(new)):
+ lines.append(
+ "" + i(name) + " : " + text(old[name]) + ""
+ )
+ for name in sorted(set(new) - set(old)):
+ lines.append(
+ i(name) + " : " + text(new[name]) + " " * 3 + i("(new)")
+ )
- for name, value in old_labels.items():
- if name not in new_labels:
- yield self.INDENTED_ITEM("{}: {}"
- .format(name, value))
+ for name in sorted(set(new) & set(old)):
+ if new[name] != old[name]:
+ lines.append(
+ i(name) + " : " + text(old[name]) + " → " + text(new[name])
+ )
- for name in old_labels:
- if name in new_labels and new_labels[name] != old_labels[name]:
- yield self.INDENTED_ITEM("{}: {} → {}"
- .format(name, old_labels[name], new_labels[name]))
+ html = ["{}
".format(header)]
+ for title, contents in filter(None, [values_section, annotate_section]):
+ section_header = "{}:
".format(title)
+ section_contents = "
\n".join(contents)
+ html.append(section_header)
+ html.append(
+ "" +
+ section_contents +
+ "
"
+ )
+ return "\n".join(html)
def abstract(var):
@@ -1308,14 +1313,13 @@ def _parse_attributes(mapping):
]).attributes
-
@singledispatch
def apply_transform(var, trs):
# type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable
"""
Apply a list of `Transform` instances on an `Orange.data.Variable`.
"""
- raise NotImplementedError
+ raise NotImplementedError # pragma: no cover
@apply_transform.register(Orange.data.DiscreteVariable)
@@ -1416,7 +1420,7 @@ def apply_transform_string(var, trs):
return variable
-def main(argv=None):
+def main(argv=None): # pragma: no cover
from AnyQt.QtWidgets import QApplication
app = QApplication(argv or [])
argv = app.arguments()
@@ -1436,5 +1440,5 @@ def main(argv=None):
return rval
-if __name__ == "__main__":
+if __name__ == "__main__": # pragma: no cover
sys.exit(main(sys.argv))
diff --git a/Orange/widgets/data/tests/test_oweditdomain.py b/Orange/widgets/data/tests/test_oweditdomain.py
index 4ae0ec6a4ad..6ef8175f365 100644
--- a/Orange/widgets/data/tests/test_oweditdomain.py
+++ b/Orange/widgets/data/tests/test_oweditdomain.py
@@ -5,78 +5,49 @@
import numpy as np
from AnyQt.QtCore import QModelIndex, QItemSelectionModel, Qt
+from AnyQt.QtWidgets import QAction
from AnyQt.QtTest import QTest
+
from Orange.data import ContinuousVariable, DiscreteVariable, \
StringVariable, TimeVariable, Table, Domain
-from Orange.widgets.data.oweditdomain import EditDomainReport, OWEditDomain, \
- ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor, \
- TimeVariableEditor, Categorical, Real, Time, String
+from Orange.widgets.data.oweditdomain import (
+ OWEditDomain,
+ ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,
+ TimeVariableEditor, Categorical, Real, Time, String,
+ Rename, Annotate, CategoriesMapping, report_transform
+)
from Orange.widgets.data.owcolor import OWColor, ColorRole
from Orange.widgets.tests.base import WidgetTest, GuiTest
-SECTION_NAME = "NAME"
-
-class TestEditDomainReport(TestCase):
- # This tests test private methods
- # pylint: disable=protected-access
-
- def setUp(self):
- self.report = EditDomainReport([], [])
-
- def test_section_yields_nothing_for_no_changes(self):
- result = self.report._section(SECTION_NAME, [])
- self.assertEmpty(result)
-
- def test_section_yields_header_for_changes(self):
- result = self.report._section(SECTION_NAME, ["a"])
- self.assertTrue(any(SECTION_NAME in item for item in result))
-
- def test_value_changes_yields_nothing_for_no_change(self):
- a = DiscreteVariable("a", values="abc")
- self.assertEmpty(self.report._value_changes(a, a))
-
- def test_value_changes_yields_nothing_for_continuous_variables(self):
- v1, v2 = ContinuousVariable("a"), ContinuousVariable("b")
- self.assertEmpty(self.report._value_changes(v1, v2))
-
- def test_value_changes_yields_changed_values(self):
- v1, v2 = DiscreteVariable("a", "ab"), DiscreteVariable("b", "ac")
- self.assertNotEmpty(self.report._value_changes(v1, v2))
-
- def test_label_changes_yields_nothing_for_no_change(self):
- v1 = ContinuousVariable("a")
- v1.attributes["a"] = "b"
- self.assertEmpty(self.report._value_changes(v1, v1))
-
- def test_label_changes_yields_added_labels(self):
- v1 = ContinuousVariable("a")
- v2 = v1.copy(None)
- v2.attributes["a"] = "b"
- self.assertNotEmpty(self.report._label_changes(v1, v2))
-
- def test_label_changes_yields_removed_labels(self):
- v1 = ContinuousVariable("a")
- v1.attributes["a"] = "b"
- v2 = v1.copy(None)
- del v2.attributes["a"]
- self.assertNotEmpty(self.report._label_changes(v1, v2))
-
- def test_label_changes_yields_modified_labels(self):
- v1 = ContinuousVariable("a")
- v1.attributes["a"] = "b"
- v2 = v1.copy(None)
- v2.attributes["a"] = "c"
- self.assertNotEmpty(self.report._label_changes(v1, v2))
-
- def assertEmpty(self, iterable):
- self.assertRaises(StopIteration, lambda: next(iter(iterable)))
-
- def assertNotEmpty(self, iterable):
- try:
- next(iter(iterable))
- except StopIteration:
- self.fail("Iterator did not produce any lines")
+class TestReport(TestCase):
+ def test_rename(self):
+ var = Real("X", (-1, ""), ())
+ tr = Rename("Y")
+ val = report_transform(var, [tr])
+ self.assertIn("X", val)
+ self.assertIn("Y", val)
+
+ def test_annotate(self):
+ var = Real("X", (-1, ""), (("a", "1"), ("b", "z")))
+ tr = Annotate((("a", "2"), ("j", "z")))
+ r = report_transform(var, [tr])
+ self.assertIn("a", r)
+ self.assertIn("b", r)
+
+ def test_categories_mapping(self):
+ var = Categorical("C", ("a", "b", "c"), None, ())
+ tr = CategoriesMapping(
+ (("a", "aa"),
+ ("b", None),
+ ("c", "cc"),
+ (None, "ee")),
+ )
+ r = report_transform(var, [tr])
+ self.assertIn("a", r)
+ self.assertIn("aa", r)
+ self.assertIn("b", r)
+ self.assertIn("", r)
class TestOWEditDomain(WidgetTest):