Repository: biolab/orange3-text

Issue:
Twitter: don't set Author as class variable
Twitter widget automatically sets Author as class variable. Don't do it. Most of the time it doesn't make sense. One can always use Select Columns to do it explicitly.


Discussion:
Knowing to use Select Columns to set the target variable is a UX issue. I mentioned this in https://github.com/biolab/orange3/issues/3997 in regard to CSV File Import, but a solution for all data set loading widgets should be devised.

Maybe if all data set widgets inherited the same base class, which contains File's attribute role controls? I guess this is more of an orange3 discussion, I'm just mentioning it here because it's relevant.

Resolving patch:
diff --git a/orangecontrib/text/twitter.py b/orangecontrib/text/twitter.py
--- a/orangecontrib/text/twitter.py
+++ b/orangecontrib/text/twitter.py
@@ -65,14 +65,14 @@ class TwitterAPI:
         argument to search method.
     """
     attributes = []
-    class_vars = [
-        (data.DiscreteVariable('Author'), lambda doc: '@' + doc.author.screen_name),
-    ]
+    class_vars = []
 
     tv = data.TimeVariable('Date')
+    authors = [(data.DiscreteVariable('Author'),
+               lambda doc: '@' + doc.author.screen_name), ]
     metas = [
         (data.StringVariable('Content'), lambda doc: doc.full_text if not
-        doc.retweeted else doc.text),
+         doc.retweeted else doc.text),
         (tv, lambda doc: TwitterAPI.tv.parse(doc.created_at.isoformat())),
         (data.DiscreteVariable('Language'), lambda doc: doc.lang),
         (data.DiscreteVariable('Location'), lambda doc: getattr(doc.place, 'country_code', None)),
@@ -162,7 +162,7 @@ def build_query():
         query = build_query()
         cursor = tweepy.Cursor(self.api.search, q=query, lang=lang,
                                tweet_mode='extended')
-        corpus, count = self.fetch(cursor, max_tweets)
+        corpus, count = self.fetch(cursor, max_tweets, search_author=False)
         self.append_history('Content', content, lang if lang else 'Any',
                             str(allow_retweets), count)
         return corpus
@@ -192,11 +192,11 @@ def search_authors(self, authors, *, max_tweets=0, collecting=False):
         cursors = [tweepy.Cursor(self.api.user_timeline, screen_name=a,
                                  tweet_mode='extended')
                    for a in authors]
-        corpus, count = self.fetch(cursors, max_tweets)
+        corpus, count = self.fetch(cursors, max_tweets, search_author=True)
         self.append_history('Author', authors, None, None, count)
         return corpus
 
-    def fetch(self, cursors, max_tweets):
+    def fetch(self, cursors, max_tweets, search_author):
         if not isinstance(cursors, list):
             cursors = [cursors]
 
@@ -221,11 +221,17 @@ def fetch(self, cursors, max_tweets):
             elif self.on_error:
                 self.on_error(str(e))
                 return None, 0
-        return self.create_corpus(), count
-
-    def create_corpus(self):
+        return self.create_corpus(search_author), count
+
+    def create_corpus(self, search_author):
+        if search_author:
+            class_vars = self.authors
+            metas = self.metas
+        else:
+            class_vars = []
+            metas = self.metas + self.authors
         return Corpus.from_documents(self.tweets, 'Twitter', self.attributes,
-                                     self.class_vars, self.metas,
+                                     class_vars, metas,
                                      title_indices=[-1])
 
     def reset(self):
diff --git a/orangecontrib/text/widgets/owtwitter.py b/orangecontrib/text/widgets/owtwitter.py
--- a/orangecontrib/text/widgets/owtwitter.py
+++ b/orangecontrib/text/widgets/owtwitter.py
@@ -90,6 +90,7 @@ class Error(OWWidget.Error):
         api = Msg('Api error ({})')
         rate_limit = Msg('Rate limit exceeded. Please try again later.')
         empty_authors = Msg('Please provide some authors.')
+        wrong_authors = Msg('Query does not match Twitter user handle.')
         key_missing = Msg('Please provide a valid API key to get the data.')
 
     tweets_info = 'Tweets on output: {}'
@@ -107,6 +108,7 @@ class Error(OWWidget.Error):
     attributes = [f.name for f in twitter.TwitterAPI.string_attributes]
     text_includes = Setting([f.name for f in twitter.TwitterAPI.text_features])
 
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.api = None
@@ -224,6 +226,9 @@ def search(self):
             if not self.word_list:
                 self.Error.empty_authors()
                 return None
+            if not any(a.startswith('@') for a in self.word_list):
+                self.Error.wrong_authors()
+                return None
             return self.api.search_authors(max_tweets=max_tweets,
                                            authors=self.word_list,
                                            collecting=self.collecting)


Test patch:
diff --git a/orangecontrib/text/tests/test_twitter.py b/orangecontrib/text/tests/test_twitter.py
--- a/orangecontrib/text/tests/test_twitter.py
+++ b/orangecontrib/text/tests/test_twitter.py
@@ -88,8 +88,7 @@ def on_progress(total, current):
         self.assertEqual(self.checker, 1)
 
     def test_create_corpus(self):
-        self.api.search_content('hello', max_tweets=5)
-        corpus = self.api.create_corpus()
+        corpus = self.api.search_content('hello', max_tweets=5)
         self.assertIsInstance(corpus, Corpus)
         self.assertEqual(len(corpus), 5)
 
diff --git a/orangecontrib/text/widgets/tests/test_owtwitter.py b/orangecontrib/text/widgets/tests/test_owtwitter.py
new file mode 100644
--- /dev/null
+++ b/orangecontrib/text/widgets/tests/test_owtwitter.py
@@ -0,0 +1,18 @@
+from Orange.widgets.tests.base import WidgetTest
+from orangecontrib.text.widgets.owtwitter import OWTwitter
+
+
+class TestTwitterWidget(WidgetTest):
+    def setUp(self):
+        self.widget = self.create_widget(OWTwitter)
+
+    def test_no_error(self):
+        self.widget.search()
+        self.assertFalse(self.widget.Error.empty_authors.is_shown())
+
+    def test_empty_author_list(self):
+        self.widget.mode = 1
+        self.widget.mode_toggle()
+        self.widget.search()
+        self.wait_until_finished()
+        self.assertTrue(self.widget.Error.empty_authors.is_shown())