From 8b4e885111b57bb4646ca141eb6a50e810814984 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Primo=C5=BE=20Godec?=
Date: Wed, 2 Jun 2021 10:11:25 +0200
Subject: [PATCH] Concordance - search in the separate thread
---
orangecontrib/text/widgets/owconcordance.py | 51 ++++++++++--------
.../text/widgets/tests/test_owconcordances.py | 52 +++++++++++++------
2 files changed, 64 insertions(+), 39 deletions(-)
diff --git a/orangecontrib/text/widgets/owconcordance.py b/orangecontrib/text/widgets/owconcordance.py
index f8bee86e0..357bc63b4 100644
--- a/orangecontrib/text/widgets/owconcordance.py
+++ b/orangecontrib/text/widgets/owconcordance.py
@@ -1,19 +1,21 @@
-from typing import Optional
+from typing import Optional, Callable
from itertools import chain
import numpy as np
from AnyQt.QtCore import Qt, QAbstractTableModel, QSize, QItemSelectionModel, \
QItemSelection, QModelIndex
-from AnyQt.QtWidgets import QSizePolicy, QApplication, QTableView, \
- QStyledItemDelegate
+from AnyQt.QtWidgets import QSizePolicy, QTableView, QStyledItemDelegate
from AnyQt.QtGui import QColor
from Orange.data import Domain, StringVariable, Table
from Orange.widgets import gui
from Orange.widgets.settings import Setting, ContextSetting, PerfectDomainContextHandler
from Orange.widgets.widget import OWWidget, Msg, Input, Output
+from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
+from Orange.util import dummy_callback
from nltk import ConcordanceIndex
+
from orangecontrib.text.corpus import Corpus
from orangecontrib.text.topics import Topic
from orangecontrib.text.preprocess import WordPunctTokenizer
@@ -73,10 +75,15 @@ def __init__(self):
self.width = 8
self.colored_rows = None
- def set_word(self, word):
+ def set_word(self, word, state: TaskState):
+ def callback(i: float):
+ state.set_progress_value(i * 100)
+ if state.is_interruption_requested():
+ raise Exception
+
self.modelAboutToBeReset.emit()
self.word = word
- self._compute_word_index()
+ self._compute_word_index(callback)
self.modelReset.emit()
def set_corpus(self, corpus):
@@ -138,13 +145,15 @@ def _compute_indices(self): # type: () -> Optional[None, list]
self.indices = [ConcordanceIndex(doc, key=lambda x: x.lower())
for doc in self.tokens]
- def _compute_word_index(self):
+ def _compute_word_index(self, callback: Callable = dummy_callback) -> None:
if self.indices is None or self.word is None:
self.word_index = self.colored_rows = None
else:
- self.word_index = [
- (doc_idx, offset) for doc_idx, doc in enumerate(self.indices)
- for offset in doc.offsets(self.word)]
+ self.word_index = []
+ for doc_idx, doc in enumerate(self.indices):
+ for offset in doc.offsets(self.word):
+ self.word_index.append((doc_idx, offset))
+ callback(doc_idx / len(self.indices))
self.colored_rows = set(sorted({d[0] for d in self.word_index})[::2])
def matching_docs(self):
@@ -169,7 +178,7 @@ def get_data(self):
return Corpus(domain, metas=conc, text_features=[domain.metas[0]])
-class OWConcordance(OWWidget):
+class OWConcordance(OWWidget, ConcurrentWidgetMixin):
name = "Concordance"
description = "Display the context of the word."
icon = "icons/Concordance.svg"
@@ -197,6 +206,7 @@ class Warning(OWWidget.Warning):
def __init__(self):
super().__init__()
+ ConcurrentWidgetMixin.__init__(self)
self.corpus = None # Corpus
self.n_matching = '' # Info on docs matching the word
@@ -217,7 +227,7 @@ def __init__(self):
gui.rubber(self.controlArea)
# Search
- c_box = gui.widgetBox(self.mainArea, orientation="vertical")
+ c_box = gui.widgetBox(self.mainArea, orientation=Qt.Horizontal)
self.input = gui.lineEdit(
c_box, self, 'word', orientation=Qt.Horizontal,
sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
@@ -296,7 +306,9 @@ def set_word_from_input(self, topic):
def set_word(self):
self.selected_rows = []
- self.model.set_word(self.word)
+ self.start(self.model.set_word, self.word)
+
+ def on_done(self, _):
self.update_widget()
self.commit()
@@ -304,8 +316,7 @@ def handleNewSignals(self):
self.set_selection(self.selected_rows)
def resize_columns(self):
- col_width = (self.conc_view.width() -
- self.conc_view.columnWidth(1)) / 2 - 12
+ col_width = (self.conc_view.width() - self.conc_view.columnWidth(1)) // 2 - 12
self.conc_view.setColumnWidth(0, col_width)
self.conc_view.setColumnWidth(2, col_width)
@@ -352,12 +363,8 @@ def send_report(self):
self.report_table(view)
-if __name__ == '__main__': # pragma: no cover
- app = QApplication([])
- widget = OWConcordance()
- corpus = Corpus.from_file('book-excerpts')
- corpus = corpus[:3]
- widget.set_corpus(corpus)
- widget.show()
- app.exec()
+if __name__ == "__main__": # pragma: no cover
+ from orangewidget.utils.widgetpreview import WidgetPreview
+ corpus = Corpus.from_file("book-excerpts")[:3]
+ WidgetPreview(OWConcordance).run(corpus)
diff --git a/orangecontrib/text/widgets/tests/test_owconcordances.py b/orangecontrib/text/widgets/tests/test_owconcordances.py
index dfaa99b34..c2a0faf91 100644
--- a/orangecontrib/text/widgets/tests/test_owconcordances.py
+++ b/orangecontrib/text/widgets/tests/test_owconcordances.py
@@ -1,15 +1,21 @@
import unittest
-from unittest.mock import Mock
+from unittest.mock import Mock, ANY
from AnyQt.QtCore import QModelIndex, QItemSelection, Qt
from AnyQt.QtGui import QBrush, QColor
-
from Orange.widgets.tests.base import WidgetTest
+from Orange.util import dummy_callback
+
from orangecontrib.text.corpus import Corpus
from orangecontrib.text.widgets.owconcordance import ConcordanceModel, \
OWConcordance
+class DummyState:
+ set_progress_value = dummy_callback
+ is_interruption_requested = lambda: False
+
+
class TestConcordanceModel(unittest.TestCase):
def setUp(self):
self.corpus = Corpus.from_file('deerwester')
@@ -21,7 +27,7 @@ def test_data(self):
self.assertEqual(model.rowCount(QModelIndex()), 0)
model.set_corpus(self.corpus)
- model.set_word("of")
+ model.set_word("of", DummyState)
# The same document in two rows
self.assertEqual(model.rowCount(QModelIndex()), 7)
@@ -47,7 +53,7 @@ def test_data_non_displayroles(self):
"""Other possibly implemented roles return correct types"""
model = ConcordanceModel()
model.set_corpus(self.corpus)
- model.set_word("of")
+ model.set_word("of", DummyState)
ind00 = model.index(0, 0)
self.assertIsInstance(model.data(ind00, Qt.ForegroundRole),
(QBrush, type(None)))
@@ -61,7 +67,7 @@ def test_color_proper_rows(self):
model = ConcordanceModel()
model.set_width(2)
model.set_corpus(self.corpus)
- model.set_word("of")
+ model.set_word("of", DummyState)
color1 = model.data(model.index(0, 0), Qt.BackgroundRole)
self.assertEqual(model.data(model.index(1, 0), Qt.BackgroundRole),
@@ -76,15 +82,15 @@ def test_order_doesnt_matter(self):
self.assertEqual(model.rowCount(QModelIndex()), 0)
model.set_corpus(self.corpus)
self.assertEqual(model.rowCount(QModelIndex()), 0)
- model.set_word("of")
+ model.set_word("of", DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 7)
- model.set_word("")
+ model.set_word("", DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 0)
- model.set_word(None)
+ model.set_word(None, DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 0)
model.set_corpus(None)
self.assertEqual(model.rowCount(QModelIndex()), 0)
- model.set_word("of")
+ model.set_word("of", DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 0)
model.set_corpus(self.corpus)
self.assertEqual(model.rowCount(QModelIndex()), 7)
@@ -100,15 +106,15 @@ def test_set_word(self):
model.set_corpus(self.corpus)
model.set_width(2)
- model.set_word("of")
+ model.set_word("of", DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 7)
self.assertEqual(model.data(model.index(0, 0)), "A survey")
- model.set_word("lab")
+ model.set_word("lab", DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 1)
self.assertEqual(model.data(model.index(0, 0)), "interface for")
- model.set_word(None)
+ model.set_word(None, DummyState)
self.assertEqual(model.rowCount(QModelIndex()), 0)
def test_signals(self):
@@ -127,19 +133,19 @@ def test_signals(self):
toBeReset.reset_mock()
hasBeenReset.reset_mock()
- model.set_word(None)
+ model.set_word(None, DummyState)
self.assertEqual(toBeReset.call_count, 1)
self.assertEqual(hasBeenReset.call_count, 1)
def test_matching_docs(self):
model = ConcordanceModel()
- model.set_word("of")
+ model.set_word("of", DummyState)
model.set_corpus(self.corpus)
self.assertEqual(model.matching_docs(), 6)
def test_concordance_output(self):
model = ConcordanceModel()
- model.set_word("of")
+ model.set_word("of", DummyState)
model.set_corpus(self.corpus)
output = model.get_data()
self.assertEqual(len(output), 7)
@@ -163,9 +169,12 @@ def test_set_corpus(self):
def test_set_word(self):
self.widget.model.set_word = set_word = Mock()
self.widget.controls.word.setText("foo")
- set_word.assert_called_with("foo")
+ self.wait_until_finished()
+ set_word.assert_called_with("foo", ANY)
+
self.widget.controls.word.setText("")
- set_word.assert_called_with("")
+ self.wait_until_finished()
+ set_word.assert_called_with("", ANY)
def test_set_width(self):
self.widget.model.set_width = set_width = Mock()
@@ -176,6 +185,8 @@ def test_selection(self):
self.send_signal("Corpus", self.corpus)
widget = self.widget
widget.controls.word.setText("of")
+ self.wait_until_finished()
+
view = self.widget.conc_view
# Select one row, two are selected, one document on the output
@@ -212,22 +223,29 @@ def test_selection(self):
view.selectRow(3)
self.assertTrue(view.selectedIndexes())
widget.controls.word.setText("o")
+ self.wait_until_finished()
self.assertFalse(view.selectedIndexes())
def test_signal_to_none(self):
self.send_signal("Corpus", self.corpus)
widget = self.widget
widget.controls.word.setText("of")
+ self.wait_until_finished()
+
view = self.widget.conc_view
nrows = widget.model.rowCount()
view.selectRow(1)
self.send_signal("Corpus", None)
+ self.wait_until_finished()
+
self.assertIsNone(self.get_output("Selected Documents"))
self.assertEqual(widget.model.rowCount(), 0)
self.assertEqual(widget.controls.word.text(), "")
self.send_signal("Corpus", self.corpus)
+ self.wait_until_finished()
+
self.assertEqual(widget.model.rowCount(), nrows)