From 45b374f24f6793a6ef2ff8bfdea17e6def89d105 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 22 Apr 2010 14:57:27 -0400 Subject: [PATCH 01/18] First stab at implementing threads... seems to work okay, then suddenly segfaults --- apithreads.py | 110 +++++++++++++++++++++++++++++++++++++++++++ mytwitter.py | 116 ++++++++++++---------------------------------- twitterwidgets.py | 2 + 3 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 apithreads.py diff --git a/apithreads.py b/apithreads.py new file mode 100644 index 0000000..b0fcb56 --- /dev/null +++ b/apithreads.py @@ -0,0 +1,110 @@ +# Python module that handles calls to the twitter API as a separate thread + +import re +from threading import Thread + +class GetTweets(Thread): + def __init__(self, api, list_name, pane, num_entries, username): + Thread.__init__(self) + self.api = api + self.list_name = list_name + self.pane = pane + self.num_entries = num_entries + self.username = username + + + def run(self): + print 'debug: GetTweets.run(): start' + + # username/Home entries need to load the appropriate Home feed + if self.list_name == self.username + '/Home': + statuses = self.api.GetHomeTimeline(count=self.num_entries) + + # For @username, check if it is one of our usernames, or + # just needs to be searched on + elif self.list_name == '@' + self.username: + statuses = self.api.GetMentions(count=self.num_entries) + elif re.match('@', self.list_name): + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + + # Direct Messages should match like /Home, above + elif self.list_name == self.username + '/Direct Messages': + statuses = dms_to_statuses(self.api.GetDirectMessages()) + + # User lookups go straight to the user + elif re.match(r'user: ', self.list_name): + statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) + + # Lists load the appropriate list from the appropriate account + elif re.match(r'list: ', self.list_name): + real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) + statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) + + # Everything else is a straight search + else: + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + + self.pane.update_window(statuses) + + print 'debug: GetTweets.run(): finished' + + +### End class GetTweets + + + +# We use these classes to emulate a Status object when we need +# one to be built out of something else. +class Status(): + def __init__(self): + self.user = User() + self.id = None + self.created_at = None + self.in_reply_to_screen_name = None + self.in_reply_to_status_id = None + + +class User(): + def __init__(self): + self.screen_name = None + self.name = None + + +# To keep things simple elsewhere and improve code reuse +# we'll build a list of home-cooked Status objects out of results. +# Why is this even necessary? +# Why can't we have more consistency out of the Twitter API? +def results_to_statuses(results): + statuses = [] + for result in results.results: + status = Status() + status.id = result.id + status.user = User() + status.user.screen_name = result.from_user + status.user.name = "" + status.in_reply_to_screen_name = result.to_user + # The Twitter Search API has different timestamps than the + # REST API... balls + # fixme: + # Gotta be a cleaner way to do this, but I can't think of it + # right now + created_at = re.sub(',', '', result.created_at) + created_split = re.split(' ', created_at) + status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3] + status.text = result.text + statuses.append(status) + return statuses + + +def dms_to_statuses(direct_messages): + statuses = [] + for dm in direct_messages: + status = Status() + status.id = dm.id + status.user = User() + status.user.screen_name = dm.sender_screen_name + status.user.name = dm.sender.name + status.created_at = dm.created_at + status.text = dm.text + statuses.append(status) + return statuses diff --git a/mytwitter.py b/mytwitter.py index 126d8ab..0d8c342 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -7,6 +7,7 @@ import twitter import gtk, gtk.glade, gobject from urllib2 import HTTPError from twitterwidgets import TweetPane +import apithreads class MyTwitter(): @@ -135,6 +136,7 @@ class MyTwitter(): gobject.timeout_add(self.refresh_time * 1000, self.update_windows) + # Spawns a thread for each pane, which updates that pane. def update_windows(self): for i in range(0, self.tweet_notebook.get_n_pages()): pane = self.tweet_notebook.get_nth_page(i) @@ -144,41 +146,36 @@ class MyTwitter(): if pane.get_single_tweet() is not None: continue - # username/Home entries need to load the appropriate Home feed - elif re.search(r'/Home', list_name): - account = self.accounts[re.sub(r'/Home', r'', list_name)] - statuses = account.GetHomeTimeline(count=self.num_entries) + # Determine username and appropriate account to use + found = False - # For @username, we need to check if it is one of our usernames, or - # just needs to be searched on - elif re.match('@', list_name): - if self.accounts.has_key(re.sub('@', '', list_name)): - account = self.accounts[re.sub(r'@', r'', list_name)] - statuses = account.GetMentions(count=self.num_entries) - else: - statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries)) - - # Direct Messages should match like /Home, above - elif re.search(r'/Direct Messages', list_name): - account = self.accounts[re.sub(r'/Direct Messages', r'', list_name)] - statuses = self.dms_to_statuses(account.GetDirectMessages()) - - # User lookups go straight to the user - elif re.match(r'user: ', list_name): - statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', list_name), count=self.num_entries) - - # Lists load the appropriate list from the appropriate account - elif re.match(r'list: ', list_name): - username = re.sub(r'list: (.*)/.*', r'\1', list_name) - real_list = re.sub(r'list: .*/(.*)', r'\1', list_name) + username = re.sub('/Home', '', list_name) + if self.accounts.has_key(username): account = self.accounts[username] - statuses = account.GetListStatuses(real_list, per_page=self.num_entries) + found = True - # Everything else is a straight search - else: - statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries)) + if not found: + username = re.sub('@', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True - pane.update_window(statuses) + if not found: + username = re.sub('/Direct Messages', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + account = self.api + username = self.username + + apithreads.GetTweets(api=account, + list_name=list_name, + pane=pane, + num_entries=self.num_entries, + username=username + ).start() # We have to return true, so the timeout_add event will keep happening return True @@ -440,69 +437,14 @@ class MyTwitter(): self.username = new_user self.api = self.accounts[self.username] - - # To keep things simple elsewhere and improve code reuse - # we'll build a list of home-cooked Status objects out of results. - # Why is this even necessary? - # Why can't we have more consistency out of the Twitter API? - def results_to_statuses(self, results): - statuses = [] - for result in results.results: - status = Status() - status.id = result.id - status.user = User() - status.user.screen_name = result.from_user - status.user.name = "" - status.in_reply_to_screen_name = result.to_user - # The Twitter Search API has different timestamps than the - # REST API... balls - # fixme: - # Gotta be a cleaner way to do this, but I can't think of it - # right now - created_at = re.sub(',', '', result.created_at) - created_split = re.split(' ', created_at) - status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3] - status.text = result.text - statuses.append(status) - return statuses - - - def dms_to_statuses(self, direct_messages): - statuses = [] - for dm in direct_messages: - status = Status() - status.id = dm.id - status.user = User() - status.user.screen_name = dm.sender_screen_name - status.user.name = dm.sender.name - status.created_at = dm.created_at - status.text = dm.text - statuses.append(status) - return statuses - ### end class MyTwitter -# We use these classes to emulate a Status object when we need -# one to be built out of something else. -class Status(): - def __init__(self): - self.user = User() - self.id = None - self.created_at = None - self.in_reply_to_screen_name = None - self.in_reply_to_status_id = None - -class User(): - def __init__(self): - self.screen_name = None - self.name = None - - # main parser = optparse.OptionParser() parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf") (options, args) = parser.parse_args() my_twitter = MyTwitter(options.filename) +gtk.gdk.threads_init() gtk.main() diff --git a/twitterwidgets.py b/twitterwidgets.py index 2077dd8..7b251db 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -70,6 +70,8 @@ class TweetPane(gtk.ScrolledWindow): def update_window(self, statuses): + print 'debug: TweetPane.update_window(): ' + self.list_name + if self.updated_once is False: self.updated_once = True From b9965dc75c77c086b9fa423c5a2bf61e9e72544f Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 22 Apr 2010 15:13:49 -0400 Subject: [PATCH 02/18] Added a little locking, seems to have fixed the segmentation fault --- apithreads.py | 7 ++++++- mytwitter.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apithreads.py b/apithreads.py index b0fcb56..5eded11 100644 --- a/apithreads.py +++ b/apithreads.py @@ -1,6 +1,7 @@ # Python module that handles calls to the twitter API as a separate thread import re +import gtk from threading import Thread class GetTweets(Thread): @@ -44,7 +45,11 @@ class GetTweets(Thread): else: statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) - self.pane.update_window(statuses) + gtk.gdk.threads_enter() + try: + self.pane.update_window(statuses) + finally: + gtk.gdk.threads_leave() print 'debug: GetTweets.run(): finished' diff --git a/mytwitter.py b/mytwitter.py index 0d8c342..c381f6a 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -446,5 +446,6 @@ parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf" (options, args) = parser.parse_args() my_twitter = MyTwitter(options.filename) + gtk.gdk.threads_init() gtk.main() From 1f16b491fa8cb406e504a3757bf56ab995ae7b08 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 22 Apr 2010 15:33:11 -0400 Subject: [PATCH 03/18] Removed now-useless debug statements --- apithreads.py | 4 ---- twitterwidgets.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/apithreads.py b/apithreads.py index 5eded11..8ebc863 100644 --- a/apithreads.py +++ b/apithreads.py @@ -15,8 +15,6 @@ class GetTweets(Thread): def run(self): - print 'debug: GetTweets.run(): start' - # username/Home entries need to load the appropriate Home feed if self.list_name == self.username + '/Home': statuses = self.api.GetHomeTimeline(count=self.num_entries) @@ -51,8 +49,6 @@ class GetTweets(Thread): finally: gtk.gdk.threads_leave() - print 'debug: GetTweets.run(): finished' - ### End class GetTweets diff --git a/twitterwidgets.py b/twitterwidgets.py index 7b251db..2077dd8 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -70,8 +70,6 @@ class TweetPane(gtk.ScrolledWindow): def update_window(self, statuses): - print 'debug: TweetPane.update_window(): ' + self.list_name - if self.updated_once is False: self.updated_once = True From 5b85ca65f4ba1ba3e5c7995b8e490f1bbc663b9c Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 22 Apr 2010 16:22:09 -0400 Subject: [PATCH 04/18] Threading makes some ugliness happen before the tweets are ready... this fixes that --- twitterwidgets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/twitterwidgets.py b/twitterwidgets.py index 2077dd8..b077254 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -55,6 +55,7 @@ class TweetPane(gtk.ScrolledWindow): self.tweets[i].connect('retweet', self.on_retweet) self.tweets[i].connect('in-reply-to', self.on_tweet_reply_to) self.tweets[i].connect('show-user', self.on_show_user) + self.tweets[i].hide() viewport.add(tweet_box) @@ -68,6 +69,9 @@ class TweetPane(gtk.ScrolledWindow): self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() + for tweet in self.tweets: + tweet.hide() + def update_window(self, statuses): if self.updated_once is False: @@ -90,8 +94,10 @@ class TweetPane(gtk.ScrolledWindow): self.num_new_tweets += 1 read = False self.tweets[i].set_status(statuses[i], read) + self.tweets[i].show() else: self.tweets[i].clear_status() + self.tweets[i].hide() try: self.latest_tweet = statuses[0].id From f838d9a61e857cf4c82b2242f064717ecb1bdff0 Mon Sep 17 00:00:00 2001 From: Anna Date: Mon, 26 Apr 2010 01:00:12 -0400 Subject: [PATCH 05/18] Documentation updates --- TODO | 2 ++ mytwitter.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 14d83de..848ce7d 100644 --- a/TODO +++ b/TODO @@ -15,3 +15,5 @@ bugs: * Direct Messages have no names, only screen names (may not be fixable without considerable tweets to python-twitter) +* Segfault on start sometimes +* Can't always kill tabs diff --git a/mytwitter.py b/mytwitter.py index c381f6a..63bffab 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -277,8 +277,7 @@ class MyTwitter(): self.tweet_notebook.set_current_page(i) return - # We check for the name so that the special case of - # the first run is handled... + # Add the pane to the persistent database of open panes if (name, single_tweet) not in self.db['open_tabs']: ot = self.db['open_tabs'] ot.append((name,single_tweet)) From 5ab60f1278d2f59dbc73c3a59fd7985fb00309a7 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 27 Apr 2010 14:53:07 -0400 Subject: [PATCH 06/18] Wrapped gtk.main() in threading code. This should fix the startup segfaults --- TODO | 1 - mytwitter.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 848ce7d..97e938d 100644 --- a/TODO +++ b/TODO @@ -15,5 +15,4 @@ bugs: * Direct Messages have no names, only screen names (may not be fixable without considerable tweets to python-twitter) -* Segfault on start sometimes * Can't always kill tabs diff --git a/mytwitter.py b/mytwitter.py index 63bffab..e9c55b6 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -447,4 +447,6 @@ parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf" my_twitter = MyTwitter(options.filename) gtk.gdk.threads_init() +gtk.gdk.threads_enter() gtk.main() +gtk.gdk.threads_leave() From 7cce83c0f3553d269ab1c9c7cca2b989edc43eac Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 27 Apr 2010 16:21:21 -0400 Subject: [PATCH 07/18] Broken commit, see new bug in TODO for details. Summary: new segfault at launch --- TODO | 3 +- apithreads.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ mytwitter.py | 45 ++++++++---------------------- twitterwidgets.py | 26 ++++++++++++++---- 4 files changed, 104 insertions(+), 40 deletions(-) diff --git a/TODO b/TODO index 97e938d..9c926ac 100644 --- a/TODO +++ b/TODO @@ -9,10 +9,11 @@ features: * Status bar icon * Support viewing a list of friends and unfollowing them * Support creating new lists and adding users to it (as well as removing users and deleting lists) - +* Need a new way to post failed tweet retrieval to the status bar bugs: * Direct Messages have no names, only screen names (may not be fixable without considerable tweets to python-twitter) * Can't always kill tabs +* New segfault at start that can't be reproduced with gdb. Must be a timing issue... adding locked access to some data in TweetPane didn't help... need locks to go with api objects. diff --git a/apithreads.py b/apithreads.py index 8ebc863..1b22523 100644 --- a/apithreads.py +++ b/apithreads.py @@ -54,6 +54,76 @@ class GetTweets(Thread): +class GetSingleTweet(Thread): + def __init__(self, api, pane, single_tweet): + Thread.__init__(self) + self.api = api + self.pane = pane + self.single_tweet = single_tweet + + + def run(self): + statuses = [] + statuses.append(self.api.GetStatus(self.single_tweet)) + + gtk.gdk.threads_enter() + try: + self.pane.update_window(statuses) + finally: + gtk.gdk.threads_leave() + + +### End class GetSingleTweet + + + +class GetFollowing(Thread): + def __init__(self, api, pane, user): + Thread.__init__(self) + self.api = api + self.pane = pane + self.user = user + + + def run(self): + screen_name = re.sub('user: ', '', self.user) + + try: + relationship = self.api.ShowFriendships(target_screen_name=screen_name) + following = relationship.source.following + except HTTPError: + following = false + + self.pane.set_following(following) + +### End class GetFollowing + + + +class GetVerified(Thread): + def __init__(self, api, pane, user): + Thread.__init__(self) + self.api = api + self.pane = pane + self.user = user + + + def run(self): + screen_name = re.sub('user: ', '', self.user) + + try: + user = self.api.GetUser(screen_name) + verified = user.verified + except HTTPError: + verified = false + + self.pane.set_verified(verified) + + +### End class GetVerified + + + # We use these classes to emulate a Status object when we need # one to be built out of something else. class Status(): diff --git a/mytwitter.py b/mytwitter.py index e9c55b6..3dbc989 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -288,9 +288,12 @@ class MyTwitter(): verified = False if re.match('user:', name): is_user = True - following = self.check_following(name) - verified = self.check_verified(name) - new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user, following=following, verified=verified) + new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user) + + if is_user: + apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start() + apithreads.GetVerified(api=self.api, pane=new_pane, user=name).start() + self.tweet_notebook.append_page_menu(new_pane, new_pane.get_tab_label(), gtk.Label(name)) self.tweet_notebook.set_tab_reorderable(new_pane, True) new_pane.get_tab_label().connect('close-clicked', self.remove_view_callback, name, single_tweet) @@ -301,14 +304,9 @@ class MyTwitter(): # Special logic for single tweet pane if single_tweet is not None: - try: - statuses = [] - statuses.append(self.api.GetStatus(single_tweet)) - new_pane.update_window(statuses) - except HTTPError: - self.tweet_notebook.remove_page(-1) - self.update_status_bar('Error retrieving tweet id: ' + str(single_tweet)) - return + apithreads.GetSingleTweet(api=self.api, + pane=new_pane, + single_tweet=single_tweet).start() self.update_windows() self.tweet_notebook.set_current_page(-1) # switch to the new pane @@ -359,34 +357,15 @@ class MyTwitter(): user_name = re.sub('^user: ', '', current_pane.get_list_name()) if current_pane.get_following(): self.api.DestroyFriendship(user_name) - current_pane.set_following(self.check_following(user_name)) + current_pane.set_following(False) else: self.api.CreateFriendship(user_name) - current_pane.set_following(self.check_following(user_name)) + current_pane.set_following(True) self.update_follow_button(current_pane) - # Name is the name of a pane, with the 'user: ' in place - def check_following(self, name): - screen_name = re.sub('user: ', '', name) - try: - relationship = self.api.ShowFriendships(target_screen_name=screen_name) - except HTTPError: - return False - return relationship.source.following - - - # Name is the name of a pane, with the 'user: ' in place - def check_verified(self, name): - screen_name = re.sub('user: ', '', name) - try: - user = self.api.GetUser(screen_name) - except HTTPError: - return False - return user.verified - - + # pane should be the currently active pane def update_follow_button(self, pane): if not pane.get_is_user(): self.following_button.set_label('') diff --git a/twitterwidgets.py b/twitterwidgets.py index b077254..9fc790d 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -1,6 +1,8 @@ import re import datetime, dateutil.tz import gtk, gobject +from threading import RLock + class TweetPane(gtk.ScrolledWindow): ''' @@ -13,9 +15,11 @@ class TweetPane(gtk.ScrolledWindow): It also gets some data from its parent, including num_entries ''' - def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False, following=False, verified=False): + def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False): gtk.ScrolledWindow.__init__(self) + self.data_lock = RLock() + self.updated_once = False self.list_name = list_name @@ -26,8 +30,8 @@ class TweetPane(gtk.ScrolledWindow): self.num_entries = 1 self.is_user = is_user - self.following = following - self.verified = verified + self.following = False + self.verified = False self.tab_label = CloseTabLabel(self.list_name) @@ -158,15 +162,25 @@ class TweetPane(gtk.ScrolledWindow): def get_following(self): - return self.following + with self.data_lock: + return self.following def get_verified(self): - return self.verified + with self.data_lock: + return self.verified def set_following(self, following): - self.following = following + print 'debug: set_following(): ' + str(following) + with self.data_lock: + self.following = following + + + def set_verified(self, verified): + print 'debug: set_verified(): ' + str(verified) + with self.data_lock: + self.verified = verified def get_is_user(self): From 6044fa27d27d7f9ca6d545694f08948e97f06224 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 4 May 2010 11:03:32 -0400 Subject: [PATCH 08/18] Added locking and moved work around / reduced repeat work at startup. Should clear up segfaults and improve startup time --- TODO | 6 ++--- apithreads.py | 69 ++++++++++++++++++++++++++++++++------------------- mytwitter.py | 25 +++++++++++-------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/TODO b/TODO index 9c926ac..c473fa7 100644 --- a/TODO +++ b/TODO @@ -14,6 +14,6 @@ features: bugs: * Direct Messages have no names, only screen names (may not be fixable without - considerable tweets to python-twitter) -* Can't always kill tabs -* New segfault at start that can't be reproduced with gdb. Must be a timing issue... adding locked access to some data in TweetPane didn't help... need locks to go with api objects. + considerable tweaks to python-twitter) +* Can't always kill tabs - open tab list not always correctly populated? +* Can't always kill application diff --git a/apithreads.py b/apithreads.py index 1b22523..baa1e95 100644 --- a/apithreads.py +++ b/apithreads.py @@ -2,7 +2,19 @@ import re import gtk -from threading import Thread +from threading import Thread,RLock +from twitter import Api + +class SafeApi(Api): + ''' This is just a Twitter API with an RLock for multi-threaded access ''' + + def __init__(self, username, password): + Api.__init__(self, username, password) + self.lock = RLock() + +# End class SafeApi + + class GetTweets(Thread): def __init__(self, api, list_name, pane, num_entries, username): @@ -15,33 +27,35 @@ class GetTweets(Thread): def run(self): - # username/Home entries need to load the appropriate Home feed - if self.list_name == self.username + '/Home': - statuses = self.api.GetHomeTimeline(count=self.num_entries) + with self.api.lock: - # For @username, check if it is one of our usernames, or - # just needs to be searched on - elif self.list_name == '@' + self.username: - statuses = self.api.GetMentions(count=self.num_entries) - elif re.match('@', self.list_name): - statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + # username/Home entries need to load the appropriate Home feed + if self.list_name == self.username + '/Home': + statuses = self.api.GetHomeTimeline(count=self.num_entries) - # Direct Messages should match like /Home, above - elif self.list_name == self.username + '/Direct Messages': - statuses = dms_to_statuses(self.api.GetDirectMessages()) + # For @username, check if it is one of our usernames, or + # just needs to be searched on + elif self.list_name == '@' + self.username: + statuses = self.api.GetMentions(count=self.num_entries) + elif re.match('@', self.list_name): + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) - # User lookups go straight to the user - elif re.match(r'user: ', self.list_name): - statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) + # Direct Messages should match like /Home, above + elif self.list_name == self.username + '/Direct Messages': + statuses = dms_to_statuses(self.api.GetDirectMessages()) - # Lists load the appropriate list from the appropriate account - elif re.match(r'list: ', self.list_name): - real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) - statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) + # User lookups go straight to the user + elif re.match(r'user: ', self.list_name): + statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) - # Everything else is a straight search - else: - statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + # Lists load the appropriate list from the appropriate account + elif re.match(r'list: ', self.list_name): + real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) + statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) + + # Everything else is a straight search + else: + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) gtk.gdk.threads_enter() try: @@ -64,7 +78,8 @@ class GetSingleTweet(Thread): def run(self): statuses = [] - statuses.append(self.api.GetStatus(self.single_tweet)) + with self.api.lock: + statuses.append(self.api.GetStatus(self.single_tweet)) gtk.gdk.threads_enter() try: @@ -89,7 +104,8 @@ class GetFollowing(Thread): screen_name = re.sub('user: ', '', self.user) try: - relationship = self.api.ShowFriendships(target_screen_name=screen_name) + with self.api.lock: + relationship = self.api.ShowFriendships(target_screen_name=screen_name) following = relationship.source.following except HTTPError: following = false @@ -112,7 +128,8 @@ class GetVerified(Thread): screen_name = re.sub('user: ', '', self.user) try: - user = self.api.GetUser(screen_name) + with self.api.lock: + user = self.api.GetUser(screen_name) verified = user.verified except HTTPError: verified = false diff --git a/mytwitter.py b/mytwitter.py index 3dbc989..6535411 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -3,7 +3,6 @@ # Custom twitter client... mostly for learning Python import sys, ConfigParser, os, re, optparse, shelve -import twitter import gtk, gtk.glade, gobject from urllib2 import HTTPError from twitterwidgets import TweetPane @@ -47,7 +46,7 @@ class MyTwitter(): for item in config.sections(): if (re.match(r'account', item)): username = config.get(item, 'username') - self.accounts[username] = twitter.Api(username=username, password=config.get(item, 'password')) + self.accounts[username] = apithreads.SafeApi(username=username, password=config.get(item, 'password')) self.username = self.accounts.keys()[0] self.api = self.accounts[self.username] @@ -106,8 +105,9 @@ class MyTwitter(): # Add the tabs from last session to the notebook page_num = self.db['active_page'] for tab, single_tweet in self.db['open_tabs']: - self.add_to_notebook(tab, single_tweet) + self.add_to_notebook(tab, single_tweet, update=False) self.tweet_notebook.set_current_page(page_num) + self.update_windows() # Put Home, @user, Direct Messages, and lists in the View menu for # each user @@ -188,7 +188,8 @@ class MyTwitter(): def update_status(self): text = self.update_entry.get_text() self.update_entry.set_text("") - self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) + with self.api.lock: + self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) self.reply_id = None self.update_status_bar('Tweet Posted') @@ -224,7 +225,8 @@ class MyTwitter(): def on_retweet(self, widget, data): - self.api.PostRetweet(data['id']) + with self.api.lock: + self.api.PostRetweet(data['id']) def on_reply_to(self, widget, data): @@ -262,7 +264,7 @@ class MyTwitter(): self.remove_view(name, single_tweet) - def add_to_notebook(self, name, single_tweet=None): + def add_to_notebook(self, name, single_tweet=None, update=True): # If it already exists, don't add it, just switch to it for i in range(self.tweet_notebook.get_n_pages()): pane = self.tweet_notebook.get_nth_page(i) @@ -308,8 +310,9 @@ class MyTwitter(): pane=new_pane, single_tweet=single_tweet).start() - self.update_windows() - self.tweet_notebook.set_current_page(-1) # switch to the new pane + if update: + self.update_windows() + self.tweet_notebook.set_current_page(-1) # switch to the new pane def on_tab_change(self, event, page, page_num): @@ -356,10 +359,12 @@ class MyTwitter(): current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page()) user_name = re.sub('^user: ', '', current_pane.get_list_name()) if current_pane.get_following(): - self.api.DestroyFriendship(user_name) + with self.api.lock: + self.api.DestroyFriendship(user_name) current_pane.set_following(False) else: - self.api.CreateFriendship(user_name) + with self.api.lock: + self.api.CreateFriendship(user_name) current_pane.set_following(True) self.update_follow_button(current_pane) From f37246f30be3013e6c58233baa423de13cb175e8 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 4 May 2010 14:32:43 -0400 Subject: [PATCH 09/18] Added error handling when API calls timeout --- apithreads.py | 57 ++++++++++++++++++++++++++--------------------- mytwitter.py | 31 ++++++++++++++++++++------ twitterwidgets.py | 10 +++++++++ 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/apithreads.py b/apithreads.py index baa1e95..bd1e4a0 100644 --- a/apithreads.py +++ b/apithreads.py @@ -4,6 +4,7 @@ import re import gtk from threading import Thread,RLock from twitter import Api +from urllib2 import HTTPError,URLError class SafeApi(Api): ''' This is just a Twitter API with an RLock for multi-threaded access ''' @@ -28,34 +29,37 @@ class GetTweets(Thread): def run(self): with self.api.lock: + try: + # username/Home entries need to load the appropriate Home feed + if self.list_name == self.username + '/Home': + statuses = self.api.GetHomeTimeline(count=self.num_entries) - # username/Home entries need to load the appropriate Home feed - if self.list_name == self.username + '/Home': - statuses = self.api.GetHomeTimeline(count=self.num_entries) + # For @username, check if it is one of our usernames, or + # just needs to be searched on + elif self.list_name == '@' + self.username: + statuses = self.api.GetMentions(count=self.num_entries) + elif re.match('@', self.list_name): + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) - # For @username, check if it is one of our usernames, or - # just needs to be searched on - elif self.list_name == '@' + self.username: - statuses = self.api.GetMentions(count=self.num_entries) - elif re.match('@', self.list_name): - statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + # Direct Messages should match like /Home, above + elif self.list_name == self.username + '/Direct Messages': + statuses = dms_to_statuses(self.api.GetDirectMessages()) - # Direct Messages should match like /Home, above - elif self.list_name == self.username + '/Direct Messages': - statuses = dms_to_statuses(self.api.GetDirectMessages()) + # User lookups go straight to the user + elif re.match(r'user: ', self.list_name): + statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) - # User lookups go straight to the user - elif re.match(r'user: ', self.list_name): - statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) + # Lists load the appropriate list from the appropriate account + elif re.match(r'list: ', self.list_name): + real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) + statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) - # Lists load the appropriate list from the appropriate account - elif re.match(r'list: ', self.list_name): - real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) - statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) + # Everything else is a straight search + else: + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) - # Everything else is a straight search - else: - statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + except HTTPError,URLError: + statuses = None gtk.gdk.threads_enter() try: @@ -79,7 +83,10 @@ class GetSingleTweet(Thread): def run(self): statuses = [] with self.api.lock: - statuses.append(self.api.GetStatus(self.single_tweet)) + try: + statuses.append(self.api.GetStatus(self.single_tweet)) + except HTTPError,URLError: + statuses = None gtk.gdk.threads_enter() try: @@ -107,7 +114,7 @@ class GetFollowing(Thread): with self.api.lock: relationship = self.api.ShowFriendships(target_screen_name=screen_name) following = relationship.source.following - except HTTPError: + except HTTPError,URLError: following = false self.pane.set_following(following) @@ -131,7 +138,7 @@ class GetVerified(Thread): with self.api.lock: user = self.api.GetUser(screen_name) verified = user.verified - except HTTPError: + except HTTPError,URLError: verified = false self.pane.set_verified(verified) diff --git a/mytwitter.py b/mytwitter.py index 6535411..9bde396 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -4,7 +4,7 @@ import sys, ConfigParser, os, re, optparse, shelve import gtk, gtk.glade, gobject -from urllib2 import HTTPError +from urllib2 import HTTPError,URLError from twitterwidgets import TweetPane import apithreads @@ -189,7 +189,13 @@ class MyTwitter(): text = self.update_entry.get_text() self.update_entry.set_text("") with self.api.lock: - self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) + try: + self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) + except HTTPError,URLError: + self.update_status_bar('Failed to post tweet') + self.reply_id = None + return + self.reply_id = None self.update_status_bar('Tweet Posted') @@ -226,7 +232,10 @@ class MyTwitter(): def on_retweet(self, widget, data): with self.api.lock: - self.api.PostRetweet(data['id']) + try: + self.api.PostRetweet(data['id']) + except HTTPError,URLError: + self.update_status_bar('Failed to retweet') def on_reply_to(self, widget, data): @@ -360,16 +369,24 @@ class MyTwitter(): user_name = re.sub('^user: ', '', current_pane.get_list_name()) if current_pane.get_following(): with self.api.lock: - self.api.DestroyFriendship(user_name) + try: + self.api.DestroyFriendship(user_name) + except HTTPError,URLError: + self.update_status_bar('Failed to unfollow user.') + return current_pane.set_following(False) else: with self.api.lock: - self.api.CreateFriendship(user_name) + try: + self.api.CreateFriendship(user_name) + except HTTPError,URLError: + self.update_status_bar('Failed to follow user.') + return current_pane.set_following(True) self.update_follow_button(current_pane) - - + + # pane should be the currently active pane def update_follow_button(self, pane): if not pane.get_is_user(): diff --git a/twitterwidgets.py b/twitterwidgets.py index 9fc790d..e9ebcf5 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -35,6 +35,8 @@ class TweetPane(gtk.ScrolledWindow): self.tab_label = CloseTabLabel(self.list_name) + self.error_message = gtk.Label('Failed to load tweet(s)') + # These handle determining which tweets are unread self.last_tweet_read = None self.latest_tweet = None @@ -52,6 +54,8 @@ class TweetPane(gtk.ScrolledWindow): viewport = gtk.Viewport() # Build us some labels... + tweet_box.pack_start(self.error_message) + for i in range(0, self.num_entries): self.tweets.append(TweetBox()) tweet_box.pack_start(self.tweets[i], expand=False) @@ -73,11 +77,17 @@ class TweetPane(gtk.ScrolledWindow): self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() + self.error_message.hide() for tweet in self.tweets: tweet.hide() def update_window(self, statuses): + if statuses is None: + self.error_message.show() + for i in range(0, self.num_entries): + self.tweets[i].hide() + if self.updated_once is False: self.updated_once = True From d2cf7c2ff5f7c103e5e3768ea88c50c7d220398b Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 4 May 2010 18:12:37 -0400 Subject: [PATCH 10/18] Added some messages to panes that display while data is still loading, or when there are errors or no data --- twitterwidgets.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/twitterwidgets.py b/twitterwidgets.py index e9ebcf5..675b48b 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -35,7 +35,7 @@ class TweetPane(gtk.ScrolledWindow): self.tab_label = CloseTabLabel(self.list_name) - self.error_message = gtk.Label('Failed to load tweet(s)') + self.message = gtk.Label('Loading...') # These handle determining which tweets are unread self.last_tweet_read = None @@ -54,7 +54,7 @@ class TweetPane(gtk.ScrolledWindow): viewport = gtk.Viewport() # Build us some labels... - tweet_box.pack_start(self.error_message) + tweet_box.pack_start(self.message) for i in range(0, self.num_entries): self.tweets.append(TweetBox()) @@ -77,16 +77,19 @@ class TweetPane(gtk.ScrolledWindow): self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() - self.error_message.hide() for tweet in self.tweets: tweet.hide() def update_window(self, statuses): if statuses is None: - self.error_message.show() + self.message.set_label('An error occurred while fetching data') + self.message.show() for i in range(0, self.num_entries): self.tweets[i].hide() + return + + self.message.hide() if self.updated_once is False: self.updated_once = True @@ -113,6 +116,9 @@ class TweetPane(gtk.ScrolledWindow): self.tweets[i].clear_status() self.tweets[i].hide() + if len(statuses) == 0: + self.message.set_label('There is no data to display') + try: self.latest_tweet = statuses[0].id except IndexError: From fe1b1cabf2d77f9962671c767894506b49414056 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 6 May 2010 13:18:02 -0400 Subject: [PATCH 11/18] Did some ApiThread refactoring, made the refresh button only refresh the current pane, and helped child threads die on exit --- TODO | 2 +- apithreads.py | 39 ++++++++++++++----------- default.glade | 2 +- mytwitter.py | 81 +++++++++++++++++++++++++++------------------------ 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/TODO b/TODO index c473fa7..95af01c 100644 --- a/TODO +++ b/TODO @@ -16,4 +16,4 @@ bugs: * Direct Messages have no names, only screen names (may not be fixable without considerable tweaks to python-twitter) * Can't always kill tabs - open tab list not always correctly populated? -* Can't always kill application +* Follow/Unfollow and Verified don't get set on current tab when it first appears... need a signal to come back from the TweetPane. diff --git a/apithreads.py b/apithreads.py index bd1e4a0..26e2bc1 100644 --- a/apithreads.py +++ b/apithreads.py @@ -17,10 +17,19 @@ class SafeApi(Api): -class GetTweets(Thread): - def __init__(self, api, list_name, pane, num_entries, username): +class ApiThread(Thread): + def __init__(self, api): Thread.__init__(self) self.api = api + self.setDaemon(True) + +# End class ApiThread + + + +class GetTweets(ApiThread): + def __init__(self, api, list_name, pane, num_entries, username): + ApiThread.__init__(self, api) self.list_name = list_name self.pane = pane self.num_entries = num_entries @@ -58,7 +67,7 @@ class GetTweets(Thread): else: statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) - except HTTPError,URLError: + except (HTTPError, URLError): statuses = None gtk.gdk.threads_enter() @@ -72,10 +81,9 @@ class GetTweets(Thread): -class GetSingleTweet(Thread): - def __init__(self, api, pane, single_tweet): - Thread.__init__(self) - self.api = api +class GetSingleTweet(ApiThread): + def __init__(self, pane, single_tweet): + ApiThread.__init__(self, api) self.pane = pane self.single_tweet = single_tweet @@ -85,7 +93,7 @@ class GetSingleTweet(Thread): with self.api.lock: try: statuses.append(self.api.GetStatus(self.single_tweet)) - except HTTPError,URLError: + except (HTTPError, URLError): statuses = None gtk.gdk.threads_enter() @@ -99,10 +107,9 @@ class GetSingleTweet(Thread): -class GetFollowing(Thread): +class GetFollowing(ApiThread): def __init__(self, api, pane, user): - Thread.__init__(self) - self.api = api + ApiThread.__init__(self, api) self.pane = pane self.user = user @@ -114,7 +121,7 @@ class GetFollowing(Thread): with self.api.lock: relationship = self.api.ShowFriendships(target_screen_name=screen_name) following = relationship.source.following - except HTTPError,URLError: + except (HTTPError, URLError): following = false self.pane.set_following(following) @@ -123,10 +130,9 @@ class GetFollowing(Thread): -class GetVerified(Thread): +class GetVerified(ApiThread): def __init__(self, api, pane, user): - Thread.__init__(self) - self.api = api + ApiThread.__init__(self, api) self.pane = pane self.user = user @@ -138,12 +144,11 @@ class GetVerified(Thread): with self.api.lock: user = self.api.GetUser(screen_name) verified = user.verified - except HTTPError,URLError: + except (HTTPError, URLError): verified = false self.pane.set_verified(verified) - ### End class GetVerified diff --git a/default.glade b/default.glade index 65666b1..ece8e70 100644 --- a/default.glade +++ b/default.glade @@ -221,7 +221,7 @@ True GTK_RELIEF_NORMAL True - + 0 diff --git a/mytwitter.py b/mytwitter.py index 9bde396..cfc6d61 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -140,49 +140,54 @@ class MyTwitter(): def update_windows(self): for i in range(0, self.tweet_notebook.get_n_pages()): pane = self.tweet_notebook.get_nth_page(i) - list_name = pane.get_list_name() - - # Single tweets should never be updated here - if pane.get_single_tweet() is not None: - continue - - # Determine username and appropriate account to use - found = False - - username = re.sub('/Home', '', list_name) - if self.accounts.has_key(username): - account = self.accounts[username] - found = True - - if not found: - username = re.sub('@', '', list_name) - if self.accounts.has_key(username): - account = self.accounts[username] - found = True - - if not found: - username = re.sub('/Direct Messages', '', list_name) - if self.accounts.has_key(username): - account = self.accounts[username] - found = True - - if not found: - account = self.api - username = self.username - - apithreads.GetTweets(api=account, - list_name=list_name, - pane=pane, - num_entries=self.num_entries, - username=username - ).start() + self.update_single_window(pane) # We have to return true, so the timeout_add event will keep happening return True - def update_windows_callback(self, widget): - self.update_windows() + def update_single_window(self, pane): + list_name = pane.get_list_name() + + # Single tweets should never be updated here + if pane.get_single_tweet() is not None: + return + + # Determine username and appropriate account to use + found = False + + username = re.sub('/Home', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + username = re.sub('@', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + username = re.sub('/Direct Messages', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + account = self.api + username = self.username + + apithreads.GetTweets(api=account, + list_name=list_name, + pane=pane, + num_entries=self.num_entries, + username=username + ).start() + + + def update_window_callback(self, widget): + pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page()) + self.update_single_window(pane) def update_status(self): From 9809e5a96aeb1d9809272993a61564cd7a3504e2 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 6 May 2010 14:19:28 -0400 Subject: [PATCH 12/18] Added signals for TweetPane setting following or verified --- twitterwidgets.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/twitterwidgets.py b/twitterwidgets.py index 675b48b..f60f46f 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -188,15 +188,15 @@ class TweetPane(gtk.ScrolledWindow): def set_following(self, following): - print 'debug: set_following(): ' + str(following) with self.data_lock: self.following = following + self.emit("following-set", following) def set_verified(self, verified): - print 'debug: set_verified(): ' + str(verified) with self.data_lock: self.verified = verified + self.emit("verified-set", verified) def get_is_user(self): @@ -219,6 +219,12 @@ gobject.signal_new("tweet-in-reply-to", TweetPane, gobject.signal_new("show-user", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) +gobject.signal_new("following-set", TweetPane, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))) +gobject.signal_new("verified-set", TweetPane, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))) From 44e1a39afeb1bef3eebec5655f8ffbd9420fbd96 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 6 May 2010 16:28:24 -0400 Subject: [PATCH 13/18] Implemented handler for the new signals, but there's a problem when the active tab is closed... --- mytwitter.py | 31 +++++++++++++++++++++++++++---- twitterwidgets.py | 8 ++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/mytwitter.py b/mytwitter.py index cfc6d61..72c0924 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -68,6 +68,9 @@ class MyTwitter(): self.reply_id = None + self.following_handler = -1 + self.verified_handler = -1 + # Load up all the GUI stuff self.init_user_interface('./default.glade') self.init_widgets() @@ -330,7 +333,18 @@ class MyTwitter(): def on_tab_change(self, event, page, page_num): + last_page = self.db['active_page'] self.db['active_page'] = page_num + + # Disconnect the old signals + if last_page != page_num: + pane = self.tweet_notebook.get_nth_page(last_page) + if self.following_handler != -1: + pane.disconnect(self.following_handler) + if self.verified_handler != -1: + pane.disconnect(self.verified_handler) + + # Now get the new page, and set everything up pane = self.tweet_notebook.get_nth_page(page_num) pane.set_tweets_read() self.update_follow_button(pane) @@ -338,10 +352,12 @@ class MyTwitter(): self.at_button.show() else: self.at_button.hide() - if pane.get_verified(): - self.verified_label.show() - else: - self.verified_label.hide() + + self.update_verified_label(pane) + + # Follow button and verified label can be returned later if this is a new pane, so set up signals + self.following_handler = pane.connect('following-set', self.update_follow_button) + self.verified_handler = pane.connect('verified-set', self.update_verified_label) def on_tabs_reordered(self, widget, child, page_num): @@ -405,6 +421,13 @@ class MyTwitter(): self.following_button.show() + def update_verified_label(self, pane): + if pane.get_verified(): + self.verified_label.show() + else: + self.verified_label.hide() + + def show_user(self, name): self.add_to_notebook('user: ' + name) diff --git a/twitterwidgets.py b/twitterwidgets.py index f60f46f..953fab6 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -190,13 +190,13 @@ class TweetPane(gtk.ScrolledWindow): def set_following(self, following): with self.data_lock: self.following = following - self.emit("following-set", following) + self.emit("following-set") def set_verified(self, verified): with self.data_lock: self.verified = verified - self.emit("verified-set", verified) + self.emit("verified-set") def get_is_user(self): @@ -221,10 +221,10 @@ gobject.signal_new("show-user", TweetPane, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("following-set", TweetPane, gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))) + gobject.TYPE_NONE, ()) gobject.signal_new("verified-set", TweetPane, gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))) + gobject.TYPE_NONE, ()) From b43bc3ccea055d61c47f12896e0e651556213910 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 6 May 2010 18:00:12 -0400 Subject: [PATCH 14/18] Made handlers for following and verified setting work correctly --- apithreads.py | 2 +- mytwitter.py | 19 ++----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/apithreads.py b/apithreads.py index 26e2bc1..fbb567e 100644 --- a/apithreads.py +++ b/apithreads.py @@ -82,7 +82,7 @@ class GetTweets(ApiThread): class GetSingleTweet(ApiThread): - def __init__(self, pane, single_tweet): + def __init__(self, api, pane, single_tweet): ApiThread.__init__(self, api) self.pane = pane self.single_tweet = single_tweet diff --git a/mytwitter.py b/mytwitter.py index 72c0924..d68b3e2 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -68,9 +68,6 @@ class MyTwitter(): self.reply_id = None - self.following_handler = -1 - self.verified_handler = -1 - # Load up all the GUI stuff self.init_user_interface('./default.glade') self.init_widgets() @@ -303,8 +300,6 @@ class MyTwitter(): self.db['open_tabs'] = ot is_user = False - following = False - verified = False if re.match('user:', name): is_user = True new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user) @@ -320,6 +315,8 @@ class MyTwitter(): new_pane.connect('tweet-retweet', self.on_retweet) new_pane.connect('tweet-in-reply-to', self.on_reply_to) new_pane.connect('show-user', self.show_user_callback) + new_pane.connect('following-set', self.update_follow_button) + new_pane.connect('verified-set', self.update_verified_label) # Special logic for single tweet pane if single_tweet is not None: @@ -336,14 +333,6 @@ class MyTwitter(): last_page = self.db['active_page'] self.db['active_page'] = page_num - # Disconnect the old signals - if last_page != page_num: - pane = self.tweet_notebook.get_nth_page(last_page) - if self.following_handler != -1: - pane.disconnect(self.following_handler) - if self.verified_handler != -1: - pane.disconnect(self.verified_handler) - # Now get the new page, and set everything up pane = self.tweet_notebook.get_nth_page(page_num) pane.set_tweets_read() @@ -355,10 +344,6 @@ class MyTwitter(): self.update_verified_label(pane) - # Follow button and verified label can be returned later if this is a new pane, so set up signals - self.following_handler = pane.connect('following-set', self.update_follow_button) - self.verified_handler = pane.connect('verified-set', self.update_verified_label) - def on_tabs_reordered(self, widget, child, page_num): self.db['active_page'] = page_num From b6a71b594a4486c31d3bc0dec5177151684b3102 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 7 May 2010 17:44:47 -0400 Subject: [PATCH 15/18] Removed unnecessary hide() function --- twitterwidgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twitterwidgets.py b/twitterwidgets.py index 953fab6..4bb90c5 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -63,7 +63,6 @@ class TweetPane(gtk.ScrolledWindow): self.tweets[i].connect('retweet', self.on_retweet) self.tweets[i].connect('in-reply-to', self.on_tweet_reply_to) self.tweets[i].connect('show-user', self.on_show_user) - self.tweets[i].hide() viewport.add(tweet_box) From a8abc21dd62cfd0750c8d5b55b91b32502af2574 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 11 May 2010 17:38:26 -0400 Subject: [PATCH 16/18] Threaded the calls to GetUserList, so that the View menu is populated asynchronously and startup time can be faster. --- apithreads.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++---- mytwitter.py | 49 +++++++++++++++++++++-------------------- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/apithreads.py b/apithreads.py index fbb567e..0c59618 100644 --- a/apithreads.py +++ b/apithreads.py @@ -1,20 +1,38 @@ # Python module that handles calls to the twitter API as a separate thread import re -import gtk +import gtk, gobject from threading import Thread,RLock from twitter import Api from urllib2 import HTTPError,URLError -class SafeApi(Api): - ''' This is just a Twitter API with an RLock for multi-threaded access ''' +class CustomApi(Api): + ''' + This is a Twitter API with an RLock for multi-threaded access + Also included is logic for processing the list names when the object is + instantiated + ''' def __init__(self, username, password): Api.__init__(self, username, password) self.lock = RLock() + self.sig_proxy = SigProxy() -# End class SafeApi + self.username = username + thread = GetUserLists(api=self) + thread.sig_proxy.connect('lists-ready', self.on_lists_ready) + thread.start() + + + def on_lists_ready(self, widget, lists, ignored): + list_names = [] + for l in lists['lists']: + list_names.append(l.name) + list_names.sort() + self.sig_proxy.emit('lists-ready', self.username, list_names) + +# End class CustomApi class ApiThread(Thread): @@ -152,6 +170,41 @@ class GetVerified(ApiThread): ### End class GetVerified +class GetUserLists(ApiThread): + def __init__(self, api): + ApiThread.__init__(self, api) + self.sig_proxy = SigProxy() + + + def run(self): + lists = [] + done = False + + while not done: + done = True + try: + with self.api.lock: + lists = self.api.GetUserLists() + except (HTTPError, URLError): + done = False + + self.sig_proxy.emit('lists-ready', lists, None) + +### End class GetUserLists + + + +class SigProxy(gtk.Alignment): + + def __init__(self): + gtk.Alignment.__init__(self) + +# End class SigProxy + +gobject.signal_new("lists-ready", SigProxy, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT)) + # We use these classes to emulate a Status object when we need # one to be built out of something else. diff --git a/mytwitter.py b/mytwitter.py index d68b3e2..b8e7253 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -46,7 +46,8 @@ class MyTwitter(): for item in config.sections(): if (re.match(r'account', item)): username = config.get(item, 'username') - self.accounts[username] = apithreads.SafeApi(username=username, password=config.get(item, 'password')) + self.accounts[username] = apithreads.CustomApi(username=username, password=config.get(item, 'password')) + self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready) self.username = self.accounts.keys()[0] self.api = self.accounts[self.username] @@ -107,31 +108,9 @@ class MyTwitter(): for tab, single_tweet in self.db['open_tabs']: self.add_to_notebook(tab, single_tweet, update=False) self.tweet_notebook.set_current_page(page_num) + self.update_windows() - # Put Home, @user, Direct Messages, and lists in the View menu for - # each user - for username in self.accounts.keys(): - outer_menu_item = gtk.MenuItem(username) - self.view_menu.append(outer_menu_item) - new_menu = gtk.Menu() - outer_menu_item.set_submenu(new_menu) - - lists = self.accounts[username].GetUserLists() - list_names = [] - for l in lists['lists']: - list_names.append(l.name) - list_names.sort() - list_names.insert(0, 'Home') - list_names.insert(1, '@' + username) - list_names.insert(2, 'Direct Messages') - for l in list_names: - menu_item = gtk.MenuItem(l) - new_menu.append(menu_item) - menu_item.connect('activate', self.on_view_selected, username, l) - menu_item.show() - outer_menu_item.show() - # Timer to update periodically gobject.timeout_add(self.refresh_time * 1000, self.update_windows) @@ -450,6 +429,28 @@ class MyTwitter(): self.username = new_user self.api = self.accounts[self.username] + + def on_lists_ready(self, widget, username, list_names): + # Setup the new sub-menu + outer_menu_item = gtk.MenuItem(username) + self.view_menu.append(outer_menu_item) + new_menu = gtk.Menu() + outer_menu_item.set_submenu(new_menu) + + # Insert the default list items + list_names.insert(0, 'Home') + list_names.insert(1, '@' + username) + list_names.insert(2, 'Direct Messages') + + # Add the items to the submenu, connect handler + for l in list_names: + menu_item = gtk.MenuItem(l) + new_menu.append(menu_item) + menu_item.connect('activate', self.on_view_selected, username, l) + menu_item.show() + + outer_menu_item.show() + ### end class MyTwitter From 8b56ed2dcfba33facb158dd00f53b17f26459220 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 11 May 2010 18:15:18 -0400 Subject: [PATCH 17/18] Fixed reply_id not getting sent to the twitter API --- mytwitter.py | 6 ++++-- twitterwidgets.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mytwitter.py b/mytwitter.py index b8e7253..1274c02 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -170,11 +170,13 @@ class MyTwitter(): def update_status(self): + reply_id = self.reply_id text = self.update_entry.get_text() self.update_entry.set_text("") + with self.api.lock: try: - self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) + self.api.PostUpdate(text, in_reply_to_status_id=reply_id) except HTTPError,URLError: self.update_status_bar('Failed to post tweet') self.reply_id = None @@ -195,7 +197,7 @@ class MyTwitter(): self.update_count.set_label(new_count) # If reply_id is set, unset it if we have removed the @ symbol - if self.reply_id is not None and not re.match('@', self.update_entry.get_text()): + if self.reply_id and not re.match('@', self.update_entry.get_text()): self.reply_id = None diff --git a/twitterwidgets.py b/twitterwidgets.py index 4bb90c5..a043a1a 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -332,7 +332,7 @@ class TweetBox(gtk.VBox): self.text.set_markup(new_text) # If this is in reply to something, set appropriate label - if self.in_reply_to_screen_name: + if self.in_reply_to_screen_name and self.in_reply_to_id: self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name) From df901febbd528d7776a23dad18d3b768f7834a0c Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 11 May 2010 22:09:54 -0400 Subject: [PATCH 18/18] Fixed a race condition. Vroom --- mytwitter.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mytwitter.py b/mytwitter.py index 1274c02..decfd2b 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -42,6 +42,10 @@ class MyTwitter(): print "Error: You must define at least one [account] section in " + config_file sys.exit(1) + # Init the glade stuff here, so we don't have a race condition with + # the lists-ready signal + self.init_user_interface('./default.glade') + self.accounts = {} for item in config.sections(): if (re.match(r'account', item)): @@ -69,8 +73,7 @@ class MyTwitter(): self.reply_id = None - # Load up all the GUI stuff - self.init_user_interface('./default.glade') + # Load up all the programmatic GUI stuff self.init_widgets() @@ -78,8 +81,6 @@ class MyTwitter(): self.widget_tree=gtk.glade.XML(path_to_skin, "window") self.widget_tree.signal_autoconnect(self) - - def init_widgets(self): # Get widgets from glade self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook') self.view_menu = self.widget_tree.get_widget('view_menu') @@ -92,6 +93,8 @@ class MyTwitter(): self.verified_label = self.widget_tree.get_widget('verified_label') self.account_select = self.widget_tree.get_widget('account_select') + + def init_widgets(self): self.context_id = self.status_bar.get_context_id('message') # Manual tweaks to the glade UI, to overcome its limitations