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 = ("") + 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):