diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 73b8ff1..62ed5c3 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -55,7 +55,7 @@ def AccountHashGenerator(customconfig): class Account(CustomConfig.ConfigHelperMixin): - """Represents an account (ie. 2 repositories) to sync + """Represents an account (ie. 2 repositories) to sync. Most of the time you will actually want to use the derived :class:`accounts.SyncableAccount` which contains all functions used @@ -71,8 +71,9 @@ class Account(CustomConfig.ConfigHelperMixin): :param config: Representing the offlineimap configuration file. :type config: :class:`offlineimap.CustomConfig.CustomConfigParser` - :param name: A string denoting the name of the Account - as configured""" + :param name: A (str) string denoting the name of the Account + as configured. + """ self.config = config self.name = name @@ -109,7 +110,7 @@ class Account(CustomConfig.ConfigHelperMixin): @classmethod def set_abort_event(cls, config, signum): - """Set skip sleep/abort event for all accounts + """Set skip sleep/abort event for all accounts. If we want to skip a current (or the next) sleep, or if we want to abort an autorefresh loop, the main thread can use @@ -121,6 +122,7 @@ class Account(CustomConfig.ConfigHelperMixin): This is a class method, it will send the signal to all accounts. """ + if signum == 1: # resync signal, set config option for all accounts for acctsection in getaccountlist(config): @@ -133,7 +135,7 @@ class Account(CustomConfig.ConfigHelperMixin): cls.abort_NOW_signal.set() def get_abort_event(self): - """Checks if an abort signal had been sent + """Checks if an abort signal had been sent. If the 'skipsleep' config option for this account had been set, with `set_abort_event(config, 1)` it will get cleared in this @@ -142,6 +144,7 @@ class Account(CustomConfig.ConfigHelperMixin): :returns: True, if the main thread had called :meth:`set_abort_event` earlier, otherwise 'False'. """ + skipsleep = self.getconfboolean("skipsleep", 0) if skipsleep: self.config.set(self.getsection(), "skipsleep", '0') @@ -149,12 +152,13 @@ class Account(CustomConfig.ConfigHelperMixin): Account.abort_NOW_signal.is_set() def _sleeper(self): - """Sleep if the account is set to autorefresh + """Sleep if the account is set to autorefresh. :returns: 0:timeout expired, 1: canceled the timer, 2:request to abort the program, 100: if configured to not sleep at all. """ + if not self.refreshperiod: return 100 @@ -184,7 +188,8 @@ class Account(CustomConfig.ConfigHelperMixin): return 0 def serverdiagnostics(self): - """Output diagnostics for all involved repositories""" + """Output diagnostics for all involved repositories.""" + remote_repo = Repository(self, 'remote') local_repo = Repository(self, 'local') #status_repo = Repository(self, 'status') @@ -194,7 +199,7 @@ class Account(CustomConfig.ConfigHelperMixin): class SyncableAccount(Account): - """A syncable email account connecting 2 repositories + """A syncable email account connecting 2 repositories. Derives from :class:`accounts.Account` but contains the additional functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`, @@ -203,11 +208,12 @@ class SyncableAccount(Account): def __init__(self, *args, **kwargs): Account.__init__(self, *args, **kwargs) self._lockfd = None - self._lockfilepath = os.path.join(self.config.getmetadatadir(), - "%s.lock" % self) + self._lockfilepath = os.path.join( + self.config.getmetadatadir(), "%s.lock"% self) def __lock(self): - """Lock the account, throwing an exception if it is locked already""" + """Lock the account, throwing an exception if it is locked already.""" + self._lockfd = open(self._lockfilepath, 'w') try: fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB) @@ -217,19 +223,19 @@ class SyncableAccount(Account): except IOError: self._lockfd.close() raise OfflineImapError("Could not lock account %s. Is another " - "instance using this account?" % self, - OfflineImapError.ERROR.REPO), \ - None, exc_info()[2] + "instance using this account?"% self, + OfflineImapError.ERROR.REPO), None, exc_info()[2] def _unlock(self): """Unlock the account, deleting the lock file""" + #If we own the lock file, delete it if self._lockfd and not self._lockfd.closed: self._lockfd.close() try: os.unlink(self._lockfilepath) except OSError: - pass #Failed to delete for some reason. + pass # Failed to delete for some reason. def syncrunner(self): self.ui.registerthread(self) @@ -265,8 +271,8 @@ class SyncableAccount(Account): raise self.ui.error(e, exc_info()[2]) except Exception as e: - self.ui.error(e, exc_info()[2], msg="While attempting to sync" - " account '%s'"% self) + self.ui.error(e, exc_info()[2], msg= + "While attempting to sync account '%s'"% self) else: # after success sync, reset the looping counter to 3 if self.refreshperiod: @@ -278,18 +284,19 @@ class SyncableAccount(Account): looping = 0 def get_local_folder(self, remotefolder): - """Return the corresponding local folder for a given remotefolder""" + """Return the corresponding local folder for a given remotefolder.""" + return self.localrepos.getfolder( remotefolder.getvisiblename(). replace(self.remoterepos.getsep(), self.localrepos.getsep())) def __sync(self): - """Synchronize the account once, then return + """Synchronize the account once, then return. Assumes that `self.remoterepos`, `self.localrepos`, and `self.statusrepos` has already been populated, so it should only - be called from the :meth:`syncrunner` function. - """ + be called from the :meth:`syncrunner` function.""" + folderthreads = [] hook = self.getconf('presynchook', '') @@ -383,12 +390,12 @@ class SyncableAccount(Account): stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) r = p.communicate() - self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r) - self.ui.callhook("Hook return code: %d" % p.returncode) + self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n"% r) + self.ui.callhook("Hook return code: %d"% p.returncode) except (KeyboardInterrupt, SystemExit): raise except Exception as e: - self.ui.error(e, exc_info()[2], msg = "Calling hook") + self.ui.error(e, exc_info()[2], msg="Calling hook") def syncfolder(account, remotefolder, quick): """Synchronizes given remote folder for the specified account. @@ -407,12 +414,12 @@ def syncfolder(account, remotefolder, quick): # Write the mailboxes mbnames.add(account.name, localfolder.getname(), - localrepos.getlocalroot()) + localrepos.getlocalroot()) # Load status folder. - statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\ - replace(remoterepos.getsep(), - statusrepos.getsep())) + statusfolder = statusrepos.getfolder(remotefolder.getvisiblename(). + replace(remoterepos.getsep(), statusrepos.getsep())) + if localfolder.get_uidvalidity() == None: # This is a new folder, so delete the status cache to be # sure we don't have a conflict. @@ -423,13 +430,13 @@ def syncfolder(account, remotefolder, quick): statusfolder.cachemessagelist() if quick: - if not localfolder.quickchanged(statusfolder) \ - and not remotefolder.quickchanged(statusfolder): + if (not localfolder.quickchanged(statusfolder) and + not remotefolder.quickchanged(statusfolder)): ui.skippingfolder(remotefolder) localrepos.restore_atime() return - # Load local folder + # Load local folder. ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder) ui.loadmessagelist(localrepos, localfolder) localfolder.cachemessagelist() @@ -488,9 +495,8 @@ def syncfolder(account, remotefolder, quick): ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' " "[acc: '%s']" % (localfolder, account)) except Exception as e: - ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \ - (account, remotefolder.getvisiblename(), - traceback.format_exc())) + ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s"% + (account, remotefolder.getvisiblename(), traceback.format_exc())) finally: for folder in ["statusfolder", "localfolder", "remotefolder"]: if folder in locals(): diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 55b2bfc..3a04ef6 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -48,13 +48,11 @@ class BaseFolder(object): self.visiblename = '' self.config = repository.getconfig() - utime_from_message_global = \ - self.config.getdefaultboolean("general", - "utime_from_message", False) + utime_from_message_global = self.config.getdefaultboolean( + "general", "utime_from_message", False) repo = "Repository " + repository.name - self._utime_from_message = \ - self.config.getdefaultboolean(repo, - "utime_from_message", utime_from_message_global) + self._utime_from_message = self.config.getdefaultboolean(repo, + "utime_from_message", utime_from_message_global) # Determine if we're running static or dynamic folder filtering # and check filtering status @@ -78,16 +76,19 @@ class BaseFolder(object): return self.name def __str__(self): + # FIMXE: remove calls of this. We have getname(). return self.name @property def accountname(self): """Account name as string""" + return self.repository.accountname @property def sync_this(self): """Should this folder be synced or is it e.g. filtered out?""" + if not self._dynamic_folderfilter: return self._sync_this else: @@ -144,7 +145,7 @@ class BaseFolder(object): if self.name == self.visiblename: return self.name else: - return "%s [remote name %s]" % (self.visiblename, self.name) + return "%s [remote name %s]"% (self.visiblename, self.name) def getrepository(self): """Returns the repository object that this folder is within.""" @@ -172,9 +173,9 @@ class BaseFolder(object): if not self.name: basename = '.' - else: #avoid directory hierarchies and file names such as '/' + else: # Avoid directory hierarchies and file names such as '/'. basename = self.name.replace('/', '.') - # replace with literal 'dot' if final path name is '.' as '.' is + # Replace with literal 'dot' if final path name is '.' as '.' is # an invalid file name. basename = re.sub('(^|\/)\.$','\\1dot', basename) return basename @@ -196,7 +197,7 @@ class BaseFolder(object): return True def _getuidfilename(self): - """provides UIDVALIDITY cache filename for class internal purposes""" + """provides UIDVALIDITY cache filename for class internal purposes. return os.path.join(self.repository.getuiddir(), self.getfolderbasename()) @@ -228,7 +229,7 @@ class BaseFolder(object): uidfilename = self._getuidfilename() with open(uidfilename + ".tmp", "wt") as file: - file.write("%d\n" % newval) + file.write("%d\n"% newval) os.rename(uidfilename + ".tmp", uidfilename) self._base_saved_uidvalidity = newval @@ -252,6 +253,7 @@ class BaseFolder(object): def getmessagelist(self): """Gets the current message list. + You must call cachemessagelist() before calling this function!""" raise NotImplementedError @@ -272,6 +274,7 @@ class BaseFolder(object): def getmessageuidlist(self): """Gets a list of UIDs. + You may have to call cachemessagelist() before calling this function!""" return self.getmessagelist().keys() @@ -377,6 +380,7 @@ class BaseFolder(object): def getmessagelabels(self, uid): """Returns the labels for the specified message.""" + raise NotImplementedError def savemessagelabels(self, uid, labels, ignorelabels=set(), mtime=0): @@ -691,10 +695,10 @@ class BaseFolder(object): # load it up. if dstfolder.storesmessages(): message = self.getmessage(uid) - #Succeeded? -> IMAP actually assigned a UID. If newid - #remained negative, no server was willing to assign us an - #UID. If newid is 0, saving succeeded, but we could not - #retrieve the new UID. Ignore message in this case. + # Succeeded? -> IMAP actually assigned a UID. If newid + # remained negative, no server was willing to assign us an + # UID. If newid is 0, saving succeeded, but we could not + # retrieve the new UID. Ignore message in this case. new_uid = dstfolder.savemessage(uid, message, flags, rtime) if new_uid > 0: if new_uid != uid: @@ -728,7 +732,7 @@ class BaseFolder(object): raise #raise on unknown errors, so we can fix those def __syncmessagesto_copy(self, dstfolder, statusfolder): - """Pass1: Copy locally existing messages not on the other side + """Pass1: Copy locally existing messages not on the other side. This will copy messages to dstfolder that exist locally but are not in the statusfolder yet. The strategy is: @@ -738,18 +742,16 @@ class BaseFolder(object): - If dstfolder doesn't have it yet, add them to dstfolder. - Update statusfolder - This function checks and protects us from action in ryrun mode. - """ + This function checks and protects us from action in dryrun mode.""" threads = [] - copylist = filter(lambda uid: not \ - statusfolder.uidexists(uid), - self.getmessageuidlist()) + copylist = filter(lambda uid: not statusfolder.uidexists(uid), + self.getmessageuidlist()) num_to_copy = len(copylist) if num_to_copy and self.repository.account.dryrun: self.ui.info("[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format( - num_to_copy, self, self.repository, dstfolder.repository)) + num_to_copy, self, self.repository, dstfolder.repository)) return for num, uid in enumerate(copylist): # bail out on CTRL-C or SIGTERM @@ -773,7 +775,7 @@ class BaseFolder(object): thread.join() def __syncmessagesto_delete(self, dstfolder, statusfolder): - """Pass 2: Remove locally deleted messages on dst + """Pass 2: Remove locally deleted messages on dst. Get all UIDS in statusfolder but not self. These are messages that were deleted in 'self'. Delete those from dstfolder and @@ -782,9 +784,8 @@ class BaseFolder(object): This function checks and protects us from action in ryrun mode. """ - deletelist = filter(lambda uid: uid>=0 \ - and not self.uidexists(uid), - statusfolder.getmessageuidlist()) + deletelist = filter(lambda uid: uid >= 0 and not + self.uidexists(uid), statusfolder.getmessageuidlist()) if len(deletelist): self.ui.deletingmessages(deletelist, [dstfolder]) if self.repository.account.dryrun: @@ -795,7 +796,7 @@ class BaseFolder(object): folder.deletemessages(deletelist) def __syncmessagesto_flags(self, dstfolder, statusfolder): - """Pass 3: Flag synchronization + """Pass 3: Flag synchronization. Compare flag mismatches in self with those in statusfolder. If msg has a valid UID and exists on dstfolder (has not e.g. been @@ -904,7 +905,7 @@ class BaseFolder(object): def __eq__(self, other): """Comparisons work either on string comparing folder names or - on the same instance + on the same instance. MailDirFolder('foo') == 'foo' --> True a = MailDirFolder('foo'); a == b --> True diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py index c12c0ff..1afbe47 100644 --- a/offlineimap/folder/Gmail.py +++ b/offlineimap/folder/Gmail.py @@ -22,11 +22,10 @@ from sys import exc_info from offlineimap import imaputil, OfflineImapError from offlineimap import imaplibutil import offlineimap.accounts - -"""Folder implementation to support features of the Gmail IMAP server. -""" from .IMAP import IMAPFolder +"""Folder implementation to support features of the Gmail IMAP server.""" + class GmailFolder(IMAPFolder): """Folder implementation to support features of the Gmail IMAP server. @@ -101,11 +100,11 @@ class GmailFolder(IMAPFolder): body = self.addmessageheader(body, '\n', self.labelsheader, labels_str) if len(body)>200: - dbg_output = "%s...%s" % (str(body)[:150], str(body)[-50:]) + dbg_output = "%s...%s"% (str(body)[:150], str(body)[-50:]) else: dbg_output = body - self.ui.debug('imap', "Returned object from fetching %d: '%s'" % + self.ui.debug('imap', "Returned object from fetching %d: '%s'"% (uid, dbg_output)) return body @@ -139,7 +138,7 @@ class GmailFolder(IMAPFolder): # imaplib2 from quoting the sequence. # # NB: msgsToFetch are sequential numbers, not UID's - res_type, response = imapobj.fetch("'%s'" % msgsToFetch, + res_type, response = imapobj.fetch("'%s'"% msgsToFetch, '(FLAGS X-GM-LABELS UID)') if res_type != 'OK': raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " % \ diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index e0c0cc7..c7e6516 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -164,10 +164,10 @@ class IMAPFolder(BaseFolder): # By default examine all messages in this folder msgsToFetch = '1:*' - maxage = self.config.getdefaultint("Account %s"% self.accountname, - "maxage", -1) - maxsize = self.config.getdefaultint("Account %s"% self.accountname, - "maxsize", -1) + maxage = self.config.getdefaultint( + "Account %s"% self.accountname, "maxage", -1) + maxsize = self.config.getdefaultint( + "Account %s"% self.accountname, "maxsize", -1) # Build search condition if (maxage != -1) | (maxsize != -1): @@ -178,9 +178,9 @@ class IMAPFolder(BaseFolder): oldest_struct = time.gmtime(time.time() - (60*60*24*maxage)) if oldest_struct[0] < 1900: raise OfflineImapError("maxage setting led to year %d. " - "Abort syncing." % oldest_struct[0], - OfflineImapError.ERROR.REPO) - search_cond += "SINCE %02d-%s-%d" % ( + "Abort syncing."% oldest_struct[0], + OfflineImapError.ERROR.REPO) + search_cond += "SINCE %02d-%s-%d"% ( oldest_struct[2], MonthNames[oldest_struct[1]], oldest_struct[0]) @@ -188,7 +188,7 @@ class IMAPFolder(BaseFolder): if(maxsize != -1): if(maxage != -1): # There are two conditions, add space search_cond += " " - search_cond += "SMALLER %d" % maxsize + search_cond += "SMALLER %d"% maxsize search_cond += ")" @@ -225,10 +225,8 @@ class IMAPFolder(BaseFolder): msgsToFetch, '(FLAGS UID)') if res_type != 'OK': raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " - "Server responded '[%s] %s'"% ( - self.getrepository(), self, - res_type, response), - OfflineImapError.ERROR.FOLDER) + "Server responded '[%s] %s'"% (self.getrepository(), self, + res_type, response), OfflineImapError.ERROR.FOLDER) finally: self.imapserver.releaseconnection(imapobj) @@ -259,7 +257,7 @@ class IMAPFolder(BaseFolder): # Interface from BaseFolder def getmessage(self, uid): - """Retrieve message with UID from the IMAP server (incl body) + """Retrieve message with UID from the IMAP server (incl body). After this function all CRLFs will be transformed to '\n'. @@ -280,7 +278,7 @@ class IMAPFolder(BaseFolder): data = data[0][1].replace(CRLF, "\n") if len(data)>200: - dbg_output = "%s...%s" % (str(data)[:150], str(data)[-50:]) + dbg_output = "%s...%s"% (str(data)[:150], str(data)[-50:]) else: dbg_output = data @@ -331,7 +329,8 @@ class IMAPFolder(BaseFolder): # Now find the UID it got. headervalue = imapobj._quote(headervalue) try: - matchinguids = imapobj.uid('search', 'HEADER', headername, headervalue)[1][0] + matchinguids = imapobj.uid('search', 'HEADER', + headername, headervalue)[1][0] except imapobj.error as err: # IMAP server doesn't implement search or had a problem. self.ui.debug('imap', "__savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s"% (err, headername)) @@ -396,8 +395,8 @@ class IMAPFolder(BaseFolder): result = imapobj.uid('FETCH', bytearray('%d:*'% start), 'rfc822.header') if result[0] != 'OK': - raise OfflineImapError('Error fetching mail headers: ' + '. '.join(result[1]), - OfflineImapError.ERROR.MESSAGE) + raise OfflineImapError('Error fetching mail headers: %s'% + '. '.join(result[1]), OfflineImapError.ERROR.MESSAGE) result = result[1] @@ -423,7 +422,8 @@ class IMAPFolder(BaseFolder): def __getmessageinternaldate(self, content, rtime=None): """Parses mail and returns an INTERNALDATE string - It will use information in the following order, falling back as an attempt fails: + It will use information in the following order, falling back as an + attempt fails: - rtime parameter - Date header of email @@ -475,21 +475,22 @@ class IMAPFolder(BaseFolder): "Server will use local time."% datetuple) return None - #produce a string representation of datetuple that works as - #INTERNALDATE + # Produce a string representation of datetuple that works as + # INTERNALDATE. num2mon = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} - #tm_isdst coming from email.parsedate is not usable, we still use it here, mhh + # tm_isdst coming from email.parsedate is not usable, we still use it + # here, mhh. if datetuple.tm_isdst == '1': zone = -time.altzone else: zone = -time.timezone offset_h, offset_m = divmod(zone//60, 60) - internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"' \ - % (datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \ - datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m) + internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"'% \ + (datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \ + datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m) return internaldate @@ -554,7 +555,7 @@ class IMAPFolder(BaseFolder): content = self.addmessageheader(content, CRLF, headername, headervalue) if len(content)>200: - dbg_output = "%s...%s" % (content[:150], content[-50:]) + dbg_output = "%s...%s"% (content[:150], content[-50:]) else: dbg_output = content self.ui.debug('imap', "savemessage: date: %s, content: '%s'"% @@ -726,6 +727,7 @@ class IMAPFolder(BaseFolder): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + imapobj = self.imapserver.acquireconnection() try: result = self._store_to_imap(imapobj, str(uid), 'FLAGS', diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py index d9cfa70..57c696d 100644 --- a/offlineimap/folder/LocalStatus.py +++ b/offlineimap/folder/LocalStatus.py @@ -21,8 +21,9 @@ import threading from .Base import BaseFolder + class LocalStatusFolder(BaseFolder): - """LocalStatus backend implemented as a plain text file""" + """LocalStatus backend implemented as a plain text file.""" cur_version = 2 magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d" @@ -53,12 +54,10 @@ class LocalStatusFolder(BaseFolder): if not self.isnewfolder(): os.unlink(self.filename) - # Interface from BaseFolder def msglist_item_initializer(self, uid): return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0} - def readstatus_v1(self, fp): """Read status folder in format version 1. @@ -80,7 +79,6 @@ class LocalStatusFolder(BaseFolder): self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid]['flags'] = flags - def readstatus(self, fp): """Read status file in the current format. @@ -97,7 +95,7 @@ class LocalStatusFolder(BaseFolder): mtime = long(mtime) labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) except ValueError as e: - errstr = "Corrupt line '%s' in cache file '%s'" % \ + errstr = "Corrupt line '%s' in cache file '%s'"% \ (line, self.filename) self.ui.warn(errstr) raise ValueError(errstr), None, exc_info()[2] @@ -227,7 +225,6 @@ class LocalStatusFolder(BaseFolder): self.messagelist[uid]['flags'] = flags self.save() - def savemessagelabels(self, uid, labels, mtime=None): self.messagelist[uid]['labels'] = labels if mtime: self.messagelist[uid]['mtime'] = mtime @@ -263,7 +260,6 @@ class LocalStatusFolder(BaseFolder): def getmessagemtime(self, uid): return self.messagelist[uid]['mtime'] - # Interface from BaseFolder def deletemessage(self, uid): self.deletemessages([uid]) diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py index 4f5efb1..8a3b9df 100644 --- a/offlineimap/folder/LocalStatusSQLite.py +++ b/offlineimap/folder/LocalStatusSQLite.py @@ -64,7 +64,7 @@ class LocalStatusSQLiteFolder(BaseFolder): #Try to establish connection, no need for threadsafety in __init__ try: - self.connection = sqlite.connect(self.filename, check_same_thread = False) + self.connection = sqlite.connect(self.filename, check_same_thread=False) except NameError: # sqlite import had failed raise UserWarning('SQLite backend chosen, but no sqlite python ' @@ -93,7 +93,6 @@ class LocalStatusSQLiteFolder(BaseFolder): def getfullname(self): return self.filename - # Interface from LocalStatusFolder def isnewfolder(self): return self._newfolder @@ -101,7 +100,8 @@ class LocalStatusSQLiteFolder(BaseFolder): # Interface from LocalStatusFolder def deletemessagelist(self): - """delete all messages in the db""" + """Delete all messages in the db.""" + self.__sql_write('DELETE FROM status') @@ -114,6 +114,7 @@ class LocalStatusSQLiteFolder(BaseFolder): :param executemany: bool indicating whether we want to perform conn.executemany() or conn.execute(). :returns: the Cursor() or raises an Exception""" + success = False while not success: self._dblock.acquire() @@ -153,8 +154,8 @@ class LocalStatusSQLiteFolder(BaseFolder): # Upgrade from database version 1 to version 2 # This change adds labels and mtime columns, to be used by Gmail IMAP and Maildir folders. if from_ver <= 1: - self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s' %\ - (self.repository, self)) + self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s'% + (self.repository, self)) self.connection.executescript("""ALTER TABLE status ADD mtime INTEGER DEFAULT 0; ALTER TABLE status ADD labels VARCHAR(256) DEFAULT ''; UPDATE metadata SET value='2' WHERE key='db_version'; @@ -167,12 +168,10 @@ class LocalStatusSQLiteFolder(BaseFolder): def __create_db(self): - """ - Create a new db file. + """Create a new db file. self.connection must point to the opened and valid SQlite - database connection. - """ + database connection.""" self.ui._msg('Creating new Local Status db for %s:%s' \ % (self.repository, self)) self.connection.executescript(""" @@ -212,6 +211,7 @@ class LocalStatusSQLiteFolder(BaseFolder): def saveall(self): """Saves the entire messagelist to the database.""" + data = [] for uid, msg in self.messagelist.items(): mtime = msg['mtime'] @@ -219,8 +219,9 @@ class LocalStatusSQLiteFolder(BaseFolder): labels = ', '.join(sorted(msg['labels'])) data.append((uid, flags, mtime, labels)) - self.__sql_write('INSERT OR REPLACE INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)', - data, executemany=True) + self.__sql_write('INSERT OR REPLACE INTO status ' + '(id,flags,mtime,labels) VALUES (?,?,?,?)', + data, executemany=True) # Following some pure SQLite functions, where we chose to use @@ -267,14 +268,12 @@ class LocalStatusSQLiteFolder(BaseFolder): # Interface from BaseFolder def savemessage(self, uid, content, flags, rtime, mtime=0, labels=set()): - """ - Writes a new message, with the specified uid. + """Writes a new message, with the specified uid. See folder/Base for detail. Note that savemessage() does not check against dryrun settings, so you need to ensure that - savemessage is never called in a dryrun mode. - - """ + savemessage is never called in a dryrun mode.""" + if uid < 0: # We cannot assign a uid. return uid @@ -352,6 +351,7 @@ class LocalStatusSQLiteFolder(BaseFolder): def savemessagesmtimebulk(self, mtimes): """Saves mtimes from the mtimes dictionary in a single database operation.""" + data = [(mt, uid) for uid, mt in mtimes.items()] self.__sql_write('UPDATE status SET mtime=? WHERE id=?', data, executemany=True) for uid, mt in mtimes.items(): @@ -376,6 +376,7 @@ class LocalStatusSQLiteFolder(BaseFolder): This function uses sqlites executemany() function which is much faster than iterating through deletemessage() when we have many messages to delete.""" + # Weed out ones not in self.messagelist uidlist = [uid for uid in uidlist if uid in self.messagelist] if not len(uidlist): diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index 3a19cc3..b35bfc2 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -69,7 +69,7 @@ class MaildirFolder(BaseFolder): "Account "+self.accountname, "maildir-windows-compatible", False) self.infosep = '!' if self.wincompatible else ':' """infosep is the separator between maildir name and flag appendix""" - self.re_flagmatch = re.compile('%s2,(\w*)' % self.infosep) + self.re_flagmatch = re.compile('%s2,(\w*)'% self.infosep) #self.ui is set in BaseFolder.init() # Everything up to the first comma or colon (or ! if Windows): self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)') @@ -128,13 +128,14 @@ class MaildirFolder(BaseFolder): detected, we return an empty flags list. :returns: (prefix, UID, FMD5, flags). UID is a numeric "long" - type. flags is a set() of Maildir flags""" + type. flags is a set() of Maildir flags. + """ prefix, uid, fmd5, flags = None, None, None, set() prefixmatch = self.re_prefixmatch.match(filename) if prefixmatch: prefix = prefixmatch.group(1) - folderstr = ',FMD5=%s' % self._foldermd5 + folderstr = ',FMD5=%s'% self._foldermd5 foldermatch = folderstr in filename # If there was no folder MD5 specified, or if it mismatches, # assume it is a foreign (new) message and ret: uid, fmd5 = None, None @@ -154,7 +155,9 @@ class MaildirFolder(BaseFolder): Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F (flagged). - :returns: dict that can be used as self.messagelist""" + :returns: dict that can be used as self.messagelist. + """ + maxage = self.config.getdefaultint("Account " + self.accountname, "maxage", None) maxsize = self.config.getdefaultint("Account " + self.accountname, @@ -254,9 +257,9 @@ class MaildirFolder(BaseFolder): :returns: String containing unique message filename""" timeval, timeseq = _gettimeseq() - return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \ + return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s'% \ (timeval, timeseq, os.getpid(), socket.gethostname(), - uid, self._foldermd5, self.infosep, ''.join(sorted(flags))) + uid, self._foldermd5, self.infosep, ''.join(sorted(flags))) def save_to_tmp_file(self, filename, content): @@ -393,7 +396,7 @@ class MaildirFolder(BaseFolder): """ if not uid in self.messagelist: - raise OfflineImapError("Cannot change unknown Maildir UID %s" % uid) + raise OfflineImapError("Cannot change unknown Maildir UID %s"% uid) if uid == new_uid: return oldfilename = self.messagelist[uid]['filename'] diff --git a/offlineimap/folder/UIDMaps.py b/offlineimap/folder/UIDMaps.py index eb4fbae..e8ca9a7 100644 --- a/offlineimap/folder/UIDMaps.py +++ b/offlineimap/folder/UIDMaps.py @@ -78,7 +78,7 @@ class MappedIMAPFolder(IMAPFolder): try: file = open(mapfilename + ".tmp", 'wt') for (key, value) in self.diskl2r.iteritems(): - file.write("%d:%d\n" % (key, value)) + file.write("%d:%d\n"% (key, value)) file.close() os.rename(mapfilename + '.tmp', mapfilename) finally: @@ -91,7 +91,7 @@ class MappedIMAPFolder(IMAPFolder): raise OfflineImapError("Could not find UID for msg '{0}' (f:'{1}'." " This is usually a bad thing and should be reported on the ma" "iling list.".format(e.args[0], self), - OfflineImapError.ERROR.MESSAGE), None, exc_info()[2] + OfflineImapError.ERROR.MESSAGE), None, exc_info()[2] # Interface from BaseFolder def cachemessagelist(self): @@ -215,8 +215,8 @@ class MappedIMAPFolder(IMAPFolder): newluid = self._mb.savemessage(-1, content, flags, rtime) if newluid < 1: - raise ValueError("Backend could not find uid for message, returned " - "%s" % newluid) + raise ValueError("Backend could not find uid for message, " + "returned %s"% newluid) self.maplock.acquire() try: self.diskl2r[newluid] = uid @@ -262,8 +262,8 @@ class MappedIMAPFolder(IMAPFolder): UID. The UIDMaps case handles this efficiently by simply changing the mappings file.""" if ruid not in self.r2l: - raise OfflineImapError("Cannot change unknown Maildir UID %s" % ruid, - OfflineImapError.ERROR.MESSAGE) + raise OfflineImapError("Cannot change unknown Maildir UID %s"% + ruid, OfflineImapError.ERROR.MESSAGE) if ruid == new_ruid: return # sanity check shortcut self.maplock.acquire() try: @@ -271,10 +271,10 @@ class MappedIMAPFolder(IMAPFolder): self.l2r[luid] = new_ruid del self.r2l[ruid] self.r2l[new_ruid] = luid - #TODO: diskl2r|r2l are a pain to sync and should be done away with - #diskl2r only contains positive UIDs, so wrap in ifs - if luid>0: self.diskl2r[luid] = new_ruid - if ruid>0: del self.diskr2l[ruid] + # TODO: diskl2r|r2l are a pain to sync and should be done away with + # diskl2r only contains positive UIDs, so wrap in ifs. + if luid > 0: self.diskl2r[luid] = new_ruid + if ruid > 0: del self.diskr2l[ruid] if new_ruid > 0: self.diskr2l[new_ruid] = luid self._savemaps(dolock = 0) finally: diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py index 917c726..83ffe9a 100644 --- a/offlineimap/imaplibutil.py +++ b/offlineimap/imaplibutil.py @@ -39,8 +39,9 @@ class UsefulIMAPMixIn(object): :returns: 'OK' on success, nothing if the folder was already selected or raises an :exc:`OfflineImapError`.""" - if self.__getselectedfolder() == mailbox and self.is_readonly == readonly \ - and not force: + if self.__getselectedfolder() == mailbox and \ + self.is_readonly == readonly and \ + not force: # No change; return. return # Wipe out all old responses, to maintain semantics with old imaplib2 diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 1275092..3d69426 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -45,7 +45,8 @@ class IMAPServer: Public instance variables are: self.: delim The server's folder delimiter. Only valid after acquireconnection() - """ + """ + GSS_STATE_STEP = 0 GSS_STATE_WRAP = 1 def __init__(self, repos): @@ -56,16 +57,16 @@ class IMAPServer: self.preauth_tunnel = repos.getpreauthtunnel() self.transport_tunnel = repos.gettransporttunnel() if self.preauth_tunnel and self.transport_tunnel: - raise OfflineImapError('%s: '% repos + \ - 'you must enable precisely one ' - 'type of tunnel (preauth or transport), ' - 'not both', OfflineImapError.ERROR.REPO) + raise OfflineImapError('%s: '% repos + + 'you must enable precisely one ' + 'type of tunnel (preauth or transport), ' + 'not both', OfflineImapError.ERROR.REPO) self.tunnel = \ - self.preauth_tunnel if self.preauth_tunnel \ - else self.transport_tunnel + self.preauth_tunnel if self.preauth_tunnel \ + else self.transport_tunnel self.username = \ - None if self.preauth_tunnel else repos.getuser() + None if self.preauth_tunnel else repos.getuser() self.user_identity = repos.get_remote_identity() self.authmechs = repos.get_auth_mechanisms() self.password = None @@ -74,7 +75,7 @@ class IMAPServer: self.usessl = repos.getssl() self.hostname = \ - None if self.preauth_tunnel else repos.gethost() + None if self.preauth_tunnel else repos.gethost() self.port = repos.getport() if self.port == None: self.port = 993 if self.usessl else 143 @@ -110,8 +111,8 @@ class IMAPServer: # get 1) configured password first 2) fall back to asking via UI self.password = self.repos.getpassword() or \ - self.ui.getpass(self.repos.getname(), self.config, - self.passworderror) + self.ui.getpass(self.repos.getname(), self.config, + self.passworderror) self.passworderror = None return self.password @@ -182,7 +183,7 @@ class IMAPServer: response = kerberos.authGSSClientResponse(self.gss_vc) rc = kerberos.authGSSClientStep(self.gss_vc, data) if rc != kerberos.AUTH_GSS_CONTINUE: - self.gss_step = self.GSS_STATE_WRAP + self.gss_step = self.GSS_STATE_WRAP elif self.gss_step == self.GSS_STATE_WRAP: rc = kerberos.authGSSClientUnwrap(self.gss_vc, data) response = kerberos.authGSSClientResponse(self.gss_vc) @@ -207,8 +208,8 @@ class IMAPServer: imapobj.starttls() except imapobj.error as e: raise OfflineImapError("Failed to start " - "TLS connection: %s" % str(e), - OfflineImapError.ERROR.REPO, None, exc_info()[2]) + "TLS connection: %s"% str(e), + OfflineImapError.ERROR.REPO, None, exc_info()[2]) ## All __authn_* procedures are helpers that do authentication. @@ -260,8 +261,8 @@ class IMAPServer: # (per RFC 2595) if 'LOGINDISABLED' in imapobj.capabilities: raise OfflineImapError("IMAP LOGIN is " - "disabled by server. Need to use SSL?", - OfflineImapError.ERROR.REPO) + "disabled by server. Need to use SSL?", + OfflineImapError.ERROR.REPO) else: self.__loginauth(imapobj) return True @@ -335,7 +336,7 @@ class IMAPServer: exc_stack )) raise OfflineImapError("All authentication types " - "failed:\n\t%s" % msg, OfflineImapError.ERROR.REPO) + "failed:\n\t%s"% msg, OfflineImapError.ERROR.REPO) if not tried_to_authn: methods = ", ".join(map( @@ -443,7 +444,7 @@ class IMAPServer: self.ui.warn(err) raise Exception(err) self.delim, self.root = \ - imaputil.imapsplit(listres[0])[1:] + imaputil.imapsplit(listres[0])[1:] self.delim = imaputil.dequote(self.delim) self.root = imaputil.dequote(self.root) @@ -474,7 +475,7 @@ class IMAPServer: if self.port != 993: reason = "Could not connect via SSL to host '%s' and non-s"\ "tandard ssl port %d configured. Make sure you connect"\ - " to the correct port." % (self.hostname, self.port) + " to the correct port."% (self.hostname, self.port) else: reason = "Unknown SSL protocol connecting to host '%s' for "\ "repository '%s'. OpenSSL responded:\n%s"\ @@ -487,7 +488,7 @@ class IMAPServer: reason = "Connection to host '%s:%d' for repository '%s' was "\ "refused. Make sure you have the right host and port "\ "configured and that you are actually able to access the "\ - "network." % (self.hostname, self.port, self.repos) + "network."% (self.hostname, self.port, self.repos) raise OfflineImapError(reason, severity), None, exc_info()[2] # Could not acquire connection to the remote; # socket.error(last_error) raised @@ -709,15 +710,15 @@ class IdleThread(object): imapobj.idle(callback=callback) else: self.ui.warn("IMAP IDLE not supported on server '%s'." - "Sleep until next refresh cycle." % imapobj.identifier) + "Sleep until next refresh cycle."% imapobj.identifier) imapobj.noop() self.stop_sig.wait() # self.stop() or IDLE callback are invoked try: # End IDLE mode with noop, imapobj can point to a dropped conn. imapobj.noop() except imapobj.abort: - self.ui.warn('Attempting NOOP on dropped connection %s' % \ - imapobj.identifier) + self.ui.warn('Attempting NOOP on dropped connection %s'% + imapobj.identifier) self.parent.releaseconnection(imapobj, True) else: self.parent.releaseconnection(imapobj) diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py index e5eb541..f1f287b 100644 --- a/offlineimap/imaputil.py +++ b/offlineimap/imaputil.py @@ -211,7 +211,7 @@ def uid_sequence(uidlist): def getrange(start, end): if start == end: return(str(start)) - return "%s:%s" % (start, end) + return "%s:%s"% (start, end) if not len(uidlist): return '' # Empty list, return start, end = None, None diff --git a/offlineimap/init.py b/offlineimap/init.py index 8b20394..7f6a679 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -227,7 +227,7 @@ class OfflineImap: 'of %s'% ', '.join(UI_LIST.keys())) if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info - #dry-run? Set [general]dry-run=True + # dry-run? Set [general]dry-run=True if options.dryrun: dryrun = config.set('general', 'dry-run', 'True') config.set_if_not_exists('general', 'dry-run', 'False') diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index d30d8a3..0cf44f8 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -115,7 +115,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object): @property def readonly(self): """Is the repository readonly?""" - return self._readonly def getlocaleval(self): @@ -123,13 +122,11 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object): def getfolders(self): """Returns a list of ALL folders on this server.""" - return [] def forgetfolders(self): """Forgets the cached list of folders, if any. Useful to run after a sync run.""" - pass def getsep(self): @@ -150,8 +147,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object): self.getconfboolean('createfolders', True) def makefolder(self, foldername): - """Create a new folder""" - + """Create a new folder.""" raise NotImplementedError def deletefolder(self, foldername): @@ -200,8 +196,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object): dst_haschanged = True # Need to refresh list except OfflineImapError as e: self.ui.error(e, exc_info()[2], - "Creating folder %s on repository %s" %\ - (src_name_t, dst_repo)) + "Creating folder %s on repository %s"% + (src_name_t, dst_repo)) raise status_repo.makefolder(src_name_t.replace(dst_repo.getsep(), status_repo.getsep())) @@ -218,8 +214,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object): # case don't create it on it: if not self.should_sync_folder(dst_name_t): self.ui.debug('', "Not creating folder '%s' (repository '%s" - "') as it would be filtered out on that repository." % - (dst_name_t, self)) + "') as it would be filtered out on that repository."% + (dst_name_t, self)) continue # get IMAPFolder and see if the reverse nametrans # works fine TODO: getfolder() works only because we diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index c0cd887..b109546 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -38,7 +38,7 @@ class IMAPRepository(BaseRepository): self.folders = None if self.getconf('sep', None): self.ui.info("The 'sep' setting is being ignored for IMAP " - "repository '%s' (it's autodetected)" % self) + "repository '%s' (it's autodetected)"% self) def startkeepalive(self): keepalivetime = self.getkeepalive() @@ -85,7 +85,7 @@ class IMAPRepository(BaseRepository): acquireconnection() or it will still be `None`""" assert self.imapserver.delim != None, "'%s' " \ "repository called getsep() before the folder separator was " \ - "queried from the server" % self + "queried from the server"% self return self.imapserver.delim def gethost(self): @@ -101,10 +101,9 @@ class IMAPRepository(BaseRepository): try: host = self.localeval.eval(host) except Exception as e: - raise OfflineImapError("remotehosteval option for repository "\ - "'%s' failed:\n%s" % (self, e), - OfflineImapError.ERROR.REPO), \ - None, exc_info()[2] + raise OfflineImapError("remotehosteval option for repository " + "'%s' failed:\n%s"% (self, e), OfflineImapError.ERROR.REPO), \ + None, exc_info()[2] if host: self._host = host return self._host @@ -115,9 +114,8 @@ class IMAPRepository(BaseRepository): return self._host # no success - raise OfflineImapError("No remote host for repository "\ - "'%s' specified." % self, - OfflineImapError.ERROR.REPO) + raise OfflineImapError("No remote host for repository " + "'%s' specified."% self, OfflineImapError.ERROR.REPO) def get_remote_identity(self): """Remote identity is used for certain SASL mechanisms @@ -139,8 +137,8 @@ class IMAPRepository(BaseRepository): for m in mechs: if m not in supported: - raise OfflineImapError("Repository %s: " % self + \ - "unknown authentication mechanism '%s'" % m, + raise OfflineImapError("Repository %s: "% self + \ + "unknown authentication mechanism '%s'"% m, OfflineImapError.ERROR.REPO) self.ui.debug('imap', "Using authentication mechanisms %s" % mechs) @@ -431,9 +429,8 @@ class IMAPRepository(BaseRepository): result = imapobj.create(foldername) if result[0] != 'OK': raise OfflineImapError("Folder '%s'[%s] could not be created. " - "Server responded: %s" % \ - (foldername, self, str(result)), - OfflineImapError.ERROR.FOLDER) + "Server responded: %s"% (foldername, self, str(result)), + OfflineImapError.ERROR.FOLDER) finally: self.imapserver.releaseconnection(imapobj) diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py index 170145b..fc34a55 100644 --- a/offlineimap/repository/LocalStatus.py +++ b/offlineimap/repository/LocalStatus.py @@ -28,13 +28,13 @@ class LocalStatusRepository(BaseRepository): # class and root for all backends self.backends = {} self.backends['sqlite'] = { - 'class': LocalStatusSQLiteFolder, - 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite') + 'class': LocalStatusSQLiteFolder, + 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite') } self.backends['plain'] = { - 'class': LocalStatusFolder, - 'root': os.path.join(account.getaccountmeta(), 'LocalStatus') + 'class': LocalStatusFolder, + 'root': os.path.join(account.getaccountmeta(), 'LocalStatus') } # Set class and root for the configured backend @@ -54,7 +54,7 @@ class LocalStatusRepository(BaseRepository): else: raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"% - (backend, self.account.name)) + (backend, self.account.name)) def import_other_backend(self, folder): for bk, dic in self.backends.items(): @@ -101,7 +101,7 @@ class LocalStatusRepository(BaseRepository): folder = self.LocalStatusFolderClass(foldername, self) - # if folder is empty, try to import data from an other backend + # If folder is empty, try to import data from an other backend. if folder.isnewfolder(): self.import_other_backend(folder) diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 58bf664..0262ba2 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -115,7 +115,7 @@ class MaildirRepository(BaseRepository): except OSError as e: if e.errno == 17 and os.path.isdir(full_path): self.debug("makefolder: '%s' already has subdir %s"% - (foldername, subdir)) + (foldername, subdir)) else: raise diff --git a/offlineimap/repository/__init__.py b/offlineimap/repository/__init__.py index 59a7bb6..0fbbc13 100644 --- a/offlineimap/repository/__init__.py +++ b/offlineimap/repository/__init__.py @@ -53,7 +53,7 @@ class Repository(object): 'GmailMaildir': GmailMaildirRepository} elif reqtype == 'status': - # create and return a LocalStatusRepository + # create and return a LocalStatusRepository. name = account.getconf('localrepository') return LocalStatusRepository(name, account) @@ -61,7 +61,7 @@ class Repository(object): errstr = "Repository type %s not supported" % reqtype raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO) - # Get repository type + # Get repository type. config = account.getconfig() try: repostype = config.get('Repository ' + name, 'type').strip() @@ -74,8 +74,8 @@ class Repository(object): try: repo = typemap[repostype] except KeyError: - errstr = "'%s' repository not supported for '%s' repositories." \ - % (repostype, reqtype) + errstr = "'%s' repository not supported for '%s' repositories."% \ + (repostype, reqtype) raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \ None, exc_info()[2] diff --git a/offlineimap/threadutil.py b/offlineimap/threadutil.py index 33dbd64..f69f8a6 100644 --- a/offlineimap/threadutil.py +++ b/offlineimap/threadutil.py @@ -32,6 +32,7 @@ from offlineimap.ui import getglobalui def semaphorereset(semaphore, originalstate): """Block until `semaphore` gets back to its original state, ie all acquired resources have been released.""" + for i in range(originalstate): semaphore.acquire() # Now release these. @@ -41,6 +42,7 @@ def semaphorereset(semaphore, originalstate): class threadlist: """Store the list of all threads in the software so it can be used to find out what's running and what's not.""" + def __init__(self): self.lock = Lock() self.list = [] @@ -98,6 +100,7 @@ def exitnotifymonitorloop(callback): while the other thread is waiting. :type callback: a callable function """ + global exitthreads do_loop = True while do_loop: @@ -116,6 +119,7 @@ def threadexited(thread): """Called when a thread exits. Main thread is aborted when this returns True.""" + ui = getglobalui() if thread.exit_exception: if isinstance(thread.exit_exception, SystemExit): @@ -139,8 +143,9 @@ class ExitNotifyThread(Thread): The thread can set instance variables self.exit_message for a human readable reason of the thread exit.""" + profiledir = None - """class variable that is set to the profile directory if required""" + """Class variable that is set to the profile directory if required.""" def __init__(self, *args, **kwargs): super(ExitNotifyThread, self).__init__(*args, **kwargs) @@ -167,7 +172,7 @@ class ExitNotifyThread(Thread): except SystemExit: pass prof.dump_stats(os.path.join(ExitNotifyThread.profiledir, - "%s_%s.prof" % (self.ident, self.getName()))) + "%s_%s.prof"% (self.ident, self.getName()))) except Exception as e: # Thread exited with Exception, store it tb = traceback.format_exc() @@ -179,6 +184,7 @@ class ExitNotifyThread(Thread): def set_exit_exception(self, exc, st=None): """Sets Exception and stacktrace of a thread, so that other threads can query its exit status""" + self._exit_exc = exc self._exit_stacktrace = st @@ -187,16 +193,19 @@ class ExitNotifyThread(Thread): """Returns the cause of the exit, one of: Exception() -- the thread aborted with this exception None -- normal termination.""" + return self._exit_exc @property def exit_stacktrace(self): """Returns a string representing the stack trace if set""" + return self._exit_stacktrace @classmethod def set_profiledir(cls, directory): """If set, will output profile information to 'directory'""" + cls.profiledir = directory @@ -210,6 +219,7 @@ instancelimitedlock = Lock() def initInstanceLimit(instancename, instancemax): """Initialize the instance-limited thread implementation to permit up to intancemax threads with the given instancename.""" + instancelimitedlock.acquire() if not instancename in instancelimitedsems: instancelimitedsems[instancename] = BoundedSemaphore(instancemax) diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py index fb3da80..ddc05ea 100644 --- a/offlineimap/ui/Curses.py +++ b/offlineimap/ui/Curses.py @@ -33,17 +33,19 @@ class CursesUtil: # iolock protects access to the self.iolock = RLock() self.tframe_lock = RLock() - """tframe_lock protects the self.threadframes manipulation to - only happen from 1 thread""" + # tframe_lock protects the self.threadframes manipulation to + # only happen from 1 thread. self.colormap = {} """dict, translating color string to curses color pair number""" def curses_colorpair(self, col_name): - """Return the curses color pair, that corresponds to the color""" + """Return the curses color pair, that corresponds to the color.""" + return curses.color_pair(self.colormap[col_name]) def init_colorpairs(self): - """initialize the curses color pairs available""" + """Initialize the curses color pairs available.""" + # set special colors 'gray' and 'banner' self.colormap['white'] = 0 #hardcoded by curses curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) @@ -66,24 +68,27 @@ class CursesUtil: curses.init_pair(i, fcol, bcol) def lock(self, block=True): - """Locks the Curses ui thread + """Locks the Curses ui thread. Can be invoked multiple times from the owning thread. Invoking from a non-owning thread blocks and waits until it has been unlocked by the owning thread.""" + return self.iolock.acquire(block) def unlock(self): - """Unlocks the Curses ui thread + """Unlocks the Curses ui thread. Decrease the lock counter by one and unlock the ui thread if the counter reaches 0. Only call this method when the calling thread owns the lock. A RuntimeError is raised if this method is called when the lock is unlocked.""" + self.iolock.release() def exec_locked(self, target, *args, **kwargs): """Perform an operation with full locking.""" + self.lock() try: target(*args, **kwargs) @@ -113,31 +118,34 @@ class CursesAccountFrame: def __init__(self, ui, account): """ :param account: An Account() or None (for eg SyncrunnerThread)""" + self.children = [] self.account = account if account else '*Control' self.ui = ui self.window = None - """Curses window associated with this acc""" + # Curses window associated with this acc. self.acc_num = None - """Account number (& hotkey) associated with this acc""" + # Account number (& hotkey) associated with this acc. self.location = 0 - """length of the account prefix string""" + # length of the account prefix string def drawleadstr(self, secs = 0): - """Draw the account status string + """Draw the account status string. secs tells us how long we are going to sleep.""" - sleepstr = '%3d:%02d' % (secs // 60, secs % 60) if secs else 'active' - accstr = '%s: [%s] %12.12s: ' % (self.acc_num, sleepstr, self.account) + + sleepstr = '%3d:%02d'% (secs // 60, secs % 60) if secs else 'active' + accstr = '%s: [%s] %12.12s: '% (self.acc_num, sleepstr, self.account) self.ui.exec_locked(self.window.addstr, 0, 0, accstr) self.location = len(accstr) def setwindow(self, curses_win, acc_num): - """Register an curses win and a hotkey as Account window + """Register an curses win and a hotkey as Account window. :param curses_win: the curses window associated with an account :param acc_num: int denoting the hotkey associated with this account.""" + self.window = curses_win self.acc_num = acc_num self.drawleadstr() @@ -147,39 +155,43 @@ class CursesAccountFrame: self.location += 1 def get_new_tframe(self): - """Create a new ThreadFrame and append it to self.children + """Create a new ThreadFrame and append it to self.children. :returns: The new ThreadFrame""" + tf = CursesThreadFrame(self.ui, self.window, self.location, 0) self.location += 1 self.children.append(tf) return tf def sleeping(self, sleepsecs, remainingsecs): - """show how long we are going to sleep and sleep + """Show how long we are going to sleep and sleep. :returns: Boolean, whether we want to abort the sleep""" + self.drawleadstr(remainingsecs) self.ui.exec_locked(self.window.refresh) time.sleep(sleepsecs) return self.account.get_abort_event() def syncnow(self): - """Request that we stop sleeping asap and continue to sync""" + """Request that we stop sleeping asap and continue to sync.""" + # if this belongs to an Account (and not *Control), set the # skipsleep pref if isinstance(self.account, offlineimap.accounts.Account): - self.ui.info("Requested synchronization for acc: %s" % self.account) - self.account.config.set('Account %s' % self.account.name, - 'skipsleep', '1') + self.ui.info("Requested synchronization for acc: %s"% self.account) + self.account.config.set('Account %s'% self.account.name, + 'skipsleep', '1') class CursesThreadFrame: - """ - curses_color: current color pair for logging""" + """curses_color: current color pair for logging.""" + def __init__(self, ui, acc_win, x, y): """ :param ui: is a Blinkenlights() instance :param acc_win: curses Account window""" + self.ui = ui self.window = acc_win self.x = x @@ -188,7 +200,10 @@ class CursesThreadFrame: def setcolor(self, color, modifier=0): """Draw the thread symbol '@' in the specified color - :param modifier: Curses modified, such as curses.A_BOLD""" + + :param modifier: Curses modified, such as curses.A_BOLD + """ + self.curses_color = modifier | self.ui.curses_colorpair(color) self.colorname = color self.display() @@ -201,7 +216,8 @@ class CursesThreadFrame: self.ui.exec_locked(locked_display) def update(self, acc_win, x, y): - """Update the xy position of the '.' (and possibly the aframe)""" + """Update the xy position of the '.' (and possibly the aframe).""" + self.window = acc_win self.y = y self.x = x @@ -213,6 +229,7 @@ class CursesThreadFrame: class InputHandler(ExitNotifyThread): """Listens for input via the curses interfaces""" + #TODO, we need to use the ugly exitnotifythread (rather than simply #threading.Thread here, so exiting this thread via the callback #handler, kills off all parents too. Otherwise, they would simply @@ -222,17 +239,18 @@ class InputHandler(ExitNotifyThread): self.char_handler = None self.ui = ui self.enabled = Event() - """We will only parse input if we are enabled""" + # We will only parse input if we are enabled. self.inputlock = RLock() - """denotes whether we should be handling the next char.""" + # denotes whether we should be handling the next char. self.start() #automatically start the thread def get_next_char(self): - """return the key pressed or -1 + """Return the key pressed or -1. Wait until `enabled` and loop internally every stdscr.timeout() msecs, releasing the inputlock. :returns: char or None if disabled while in here""" + self.enabled.wait() while self.enabled.is_set(): with self.inputlock: @@ -247,13 +265,14 @@ class InputHandler(ExitNotifyThread): #curses.ungetch(char) def set_char_hdlr(self, callback): - """Sets a character callback handler + """Sets a character callback handler. If a key is pressed it will be passed to this handler. Keys include the curses.KEY_RESIZE key. callback is a function taking a single arg -- the char pressed. If callback is None, input will be ignored.""" + with self.inputlock: self.char_handler = callback # start or stop the parsing of things @@ -266,13 +285,14 @@ class InputHandler(ExitNotifyThread): """Call this method when you want exclusive input control. Make sure to call input_release afterwards! While this lockis - held, input can go to e.g. the getpass input. - """ + held, input can go to e.g. the getpass input.""" + self.enabled.clear() self.inputlock.acquire() def input_release(self): """Call this method when you are done getting input.""" + self.inputlock.release() self.enabled.set() @@ -301,7 +321,7 @@ class CursesLogHandler(logging.StreamHandler): self.ui.stdscr.refresh() class Blinkenlights(UIBase, CursesUtil): - """Curses-cased fancy UI + """Curses-cased fancy UI. Notable instance variables self. ....: @@ -319,7 +339,7 @@ class Blinkenlights(UIBase, CursesUtil): ################################################## UTILS def setup_consolehandler(self): - """Backend specific console handler + """Backend specific console handler. Sets up things and adds them to self.logger. :returns: The logging.Handler() for console output""" @@ -337,7 +357,7 @@ class Blinkenlights(UIBase, CursesUtil): return ch def isusable(s): - """Returns true if the backend is usable ie Curses works""" + """Returns true if the backend is usable ie Curses works.""" # Not a terminal? Can't use curses. if not sys.stdout.isatty() and sys.stdin.isatty(): @@ -393,7 +413,7 @@ class Blinkenlights(UIBase, CursesUtil): self.info(offlineimap.banner) def acct(self, *args): - """Output that we start syncing an account (and start counting)""" + """Output that we start syncing an account (and start counting).""" self.gettf().setcolor('purple') super(Blinkenlights, self).acct(*args) @@ -458,7 +478,8 @@ class Blinkenlights(UIBase, CursesUtil): super(Blinkenlights, self).threadExited(thread) def gettf(self): - """Return the ThreadFrame() of the current thread""" + """Return the ThreadFrame() of the current thread.""" + cur_thread = currentThread() acc = self.getthreadaccount() #Account() or None @@ -504,7 +525,7 @@ class Blinkenlights(UIBase, CursesUtil): def sleep(self, sleepsecs, account): self.gettf().setcolor('red') - self.info("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) + self.info("Next sync in %d:%02d"% (sleepsecs / 60, sleepsecs % 60)) return super(Blinkenlights, self).sleep(sleepsecs, account) def sleeping(self, sleepsecs, remainingsecs): @@ -515,13 +536,14 @@ class Blinkenlights(UIBase, CursesUtil): return accframe.sleeping(sleepsecs, remainingsecs) def resizeterm(self): - """Resize the current windows""" + """Resize the current windows.""" + self.exec_locked(self.setupwindows, True) def mainException(self): UIBase.mainException(self) - def getpass(self, accountname, config, errmsg = None): + def getpass(self, accountname, config, errmsg=None): # disable the hotkeys inputhandler self.inputhandler.input_acquire() @@ -540,10 +562,11 @@ class Blinkenlights(UIBase, CursesUtil): return password def setupwindows(self, resize=False): - """Setup and draw bannerwin and logwin + """Setup and draw bannerwin and logwin. If `resize`, don't create new windows, just adapt size. This function should be invoked with CursesUtils.locked().""" + self.height, self.width = self.stdscr.getmaxyx() self.logheight = self.height - len(self.accframes) - 1 if resize: @@ -571,22 +594,24 @@ class Blinkenlights(UIBase, CursesUtil): curses.doupdate() def draw_bannerwin(self): - """Draw the top-line banner line""" + """Draw the top-line banner line.""" + if curses.has_colors(): color = curses.A_BOLD | self.curses_colorpair('banner') else: color = curses.A_REVERSE self.bannerwin.clear() # Delete old content (eg before resizes) self.bannerwin.bkgd(' ', color) # Fill background with that color - string = "%s %s" % (offlineimap.__productname__, - offlineimap.__bigversion__) + string = "%s %s"% (offlineimap.__productname__, + offlineimap.__bigversion__) self.bannerwin.addstr(0, 0, string, color) self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1, offlineimap.__copyright__, color) self.bannerwin.noutrefresh() def draw_logwin(self): - """(Re)draw the current logwindow""" + """(Re)draw the current logwindow.""" + if curses.has_colors(): color = curses.color_pair(0) #default colors else: @@ -596,9 +621,10 @@ class Blinkenlights(UIBase, CursesUtil): self.logwin.bkgd(' ', color) def getaccountframe(self, acc_name): - """Return an AccountFrame() corresponding to acc_name + """Return an AccountFrame() corresponding to acc_name. Note that the *control thread uses acc_name `None`.""" + with self.aflock: # 1) Return existing or 2) create a new CursesAccountFrame. if acc_name in self.accframes: return self.accframes[acc_name] diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index 41ce1a4..9285b51 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -36,14 +36,18 @@ debugtypes = {'':'Other offlineimap related sync messages', globalui = None def setglobalui(newui): - """Set the global ui object to be used for logging""" + """Set the global ui object to be used for logging.""" + global globalui globalui = newui + def getglobalui(): - """Return the current ui object""" + """Return the current ui object.""" + global globalui return globalui + class UIBase(object): def __init__(self, config, loglevel=logging.INFO): self.config = config @@ -65,11 +69,11 @@ class UIBase(object): self.logger = logging.getLogger('OfflineImap') self.logger.setLevel(loglevel) self._log_con_handler = self.setup_consolehandler() - """The console handler (we need access to be able to lock it)""" + """The console handler (we need access to be able to lock it).""" ################################################## UTILS def setup_consolehandler(self): - """Backend specific console handler + """Backend specific console handler. Sets up things and adds them to self.logger. :returns: The logging.Handler() for console output""" @@ -86,10 +90,11 @@ class UIBase(object): return ch def setlogfile(self, logfile): - """Create file handler which logs to file""" + """Create file handler which logs to file.""" + fh = logging.FileHandler(logfile, 'at') file_formatter = logging.Formatter("%(asctime)s %(levelname)s: " - "%(message)s", '%Y-%m-%d %H:%M:%S') + "%(message)s", '%Y-%m-%d %H:%M:%S') fh.setFormatter(file_formatter) self.logger.addHandler(fh) # write out more verbose initial info blurb on the log file @@ -107,13 +112,14 @@ class UIBase(object): def info(self, msg): """Display a message.""" + self.logger.info(msg) - def warn(self, msg, minor = 0): + def warn(self, msg, minor=0): self.logger.warning(msg) def error(self, exc, exc_traceback=None, msg=None): - """Log a message at severity level ERROR + """Log a message at severity level ERROR. Log Exception 'exc' to error log, possibly prepended by a preceding error "msg", detailing at what point the error occurred. @@ -134,7 +140,7 @@ class UIBase(object): One example of such a call might be: ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in " - "repo %s") + "repo %s") """ if msg: self.logger.error("ERROR: %s\n %s" % (msg, exc)) @@ -160,8 +166,8 @@ class UIBase(object): "'%s')" % (cur_thread.getName(), self.getthreadaccount(cur_thread), account)) else: - self.debug('thread', "Register new thread '%s' (account '%s')" %\ - (cur_thread.getName(), account)) + self.debug('thread', "Register new thread '%s' (account '%s')"% + (cur_thread.getName(), account)) self.threadaccounts[cur_thread] = account def unregisterthread(self, thr): @@ -216,7 +222,7 @@ class UIBase(object): self.warn("Invalid debug type: %s" % debugtype) def getnicename(self, object): - """Return the type of a repository or Folder as string + """Return the type of a repository or Folder as string. (IMAP, Gmail, Maildir, etc...)""" @@ -234,49 +240,49 @@ class UIBase(object): ################################################## INPUT def getpass(self, accountname, config, errmsg = None): - raise NotImplementedError("Prompting for a password is not supported"\ - " in this UI backend.") + raise NotImplementedError("Prompting for a password is not supported" + " in this UI backend.") def folderlist(self, folder_list): return ', '.join(["%s[%s]"% \ - (self.getnicename(x), x.getname()) for x in folder_list]) + (self.getnicename(x), x.getname()) for x in folder_list]) ################################################## WARNINGS def msgtoreadonly(self, destfolder, uid, content, flags): if self.config.has_option('general', 'ignore-readonly') and \ - self.config.getboolean('general', 'ignore-readonly'): + self.config.getboolean('general', 'ignore-readonly'): return self.warn("Attempted to synchronize message %d to folder %s[%s], " - "but that folder is read-only. The message will not be " - "copied to that folder." % ( - uid, self.getnicename(destfolder), destfolder)) + "but that folder is read-only. The message will not be " + "copied to that folder."% ( + uid, self.getnicename(destfolder), destfolder)) def flagstoreadonly(self, destfolder, uidlist, flags): if self.config.has_option('general', 'ignore-readonly') and \ self.config.getboolean('general', 'ignore-readonly'): return self.warn("Attempted to modify flags for messages %s in folder %s[%s], " - "but that folder is read-only. No flags have been modified " - "for that message." % ( - str(uidlist), self.getnicename(destfolder), destfolder)) + "but that folder is read-only. No flags have been modified " + "for that message."% ( + str(uidlist), self.getnicename(destfolder), destfolder)) def labelstoreadonly(self, destfolder, uidlist, labels): if self.config.has_option('general', 'ignore-readonly') and \ self.config.getboolean('general', 'ignore-readonly'): return self.warn("Attempted to modify labels for messages %s in folder %s[%s], " - "but that folder is read-only. No labels have been modified " - "for that message." % ( - str(uidlist), self.getnicename(destfolder), destfolder)) + "but that folder is read-only. No labels have been modified " + "for that message."% ( + str(uidlist), self.getnicename(destfolder), destfolder)) def deletereadonly(self, destfolder, uidlist): if self.config.has_option('general', 'ignore-readonly') and \ self.config.getboolean('general', 'ignore-readonly'): return self.warn("Attempted to delete messages %s in folder %s[%s], but that " - "folder is read-only. No messages have been deleted in that " - "folder." % (str(uidlist), self.getnicename(destfolder), - destfolder)) + "folder is read-only. No messages have been deleted in that " + "folder."% (str(uidlist), self.getnicename(destfolder), + destfolder)) ################################################## MESSAGES @@ -293,7 +299,7 @@ class UIBase(object): if not self.logger.isEnabledFor(logging.INFO): return displaystr = '' hostname = hostname if hostname else '' - port = "%s" % port if port else '' + port = "%s"% port if port else '' if hostname: displaystr = ' to %s:%s' % (hostname, port) self.logger.info("Establishing connection%s" % displaystr) @@ -309,8 +315,8 @@ class UIBase(object): sec = time.time() - self.acct_startimes[account] del self.acct_startimes[account] - self.logger.info("*** Finished account '%s' in %d:%02d" % - (account, sec // 60, sec % 60)) + self.logger.info("*** Finished account '%s' in %d:%02d"% + (account, sec // 60, sec % 60)) def syncfolders(self, src_repo, dst_repo): """Log 'Copying folder structure...'.""" @@ -321,16 +327,18 @@ class UIBase(object): ############################## Folder syncing def makefolder(self, repo, foldername): - """Called when a folder is created""" + """Called when a folder is created.""" + prefix = "[DRYRUN] " if self.dryrun else "" - self.info("{0}Creating folder {1}[{2}]".format( - prefix, foldername, repo)) + self.info(("{0}Creating folder {1}[{2}]".format( + prefix, foldername, repo))) def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder): """Called when a folder sync operation is started.""" - self.logger.info("Syncing %s: %s -> %s" % (srcfolder, - self.getnicename(srcrepos), - self.getnicename(destrepos))) + + self.logger.info("Syncing %s: %s -> %s"% (srcfolder, + self.getnicename(srcrepos), + self.getnicename(destrepos))) def skippingfolder(self, folder): """Called when a folder sync operation is started.""" @@ -344,7 +352,7 @@ class UIBase(object): folder.get_saveduidvalidity(), folder.get_uidvalidity())) def loadmessagelist(self, repos, folder): - self.logger.debug(u"Loading message list for %s[%s]"% ( + self.logger.debug("Loading message list for %s[%s]"% ( self.getnicename(repos), folder)) @@ -369,8 +377,8 @@ class UIBase(object): ds = self.folderlist(destlist) prefix = "[DRYRUN] " if self.dryrun else "" self.info("{0}Deleting {1} messages ({2}) in {3}".format( - prefix, len(uidlist), - offlineimap.imaputil.uid_sequence(uidlist), ds)) + prefix, len(uidlist), + offlineimap.imaputil.uid_sequence(uidlist), ds)) def addingflags(self, uidlist, flags, dest): self.logger.info("Adding flag %s to %d messages on %s" % ( @@ -407,9 +415,8 @@ class UIBase(object): self.getnicename(repository))) try: if hasattr(repository, 'gethost'): # IMAP - self._msg("Host: %s Port: %s SSL: %s" % (repository.gethost(), - repository.getport(), - repository.getssl())) + self._msg("Host: %s Port: %s SSL: %s"% (repository.gethost(), + repository.getport(), repository.getssl())) try: conn = repository.imapserver.acquireconnection() except OfflineImapError as e: @@ -437,8 +444,8 @@ class UIBase(object): self._msg("nametrans= %s\n" % nametrans) folders = repository.getfolders() - foldernames = [(f.name, f.getvisiblename(), f.sync_this) \ - for f in folders] + foldernames = [(f.name, f.getvisiblename(), f.sync_this) + for f in folders] folders = [] for name, visiblename, sync_this in foldernames: syncstr = "" if sync_this else " (disabled)" @@ -454,8 +461,8 @@ class UIBase(object): def savemessage(self, debugtype, uid, flags, folder): """Output a log line stating that we save a msg.""" - self.debug(debugtype, u"Write mail '%s:%d' with flags %s"% - (folder, uid, repr(flags))) + self.debug(debugtype, "Write mail '%s:%d' with flags %s"% + (folder, uid, repr(flags))) ################################################## Threads @@ -465,8 +472,8 @@ class UIBase(object): % (len(self.debugmessages[thread]), thread.getName()) message += "\n".join(self.debugmessages[thread]) else: - message = "\nNo debug messages were logged for %s." % \ - thread.getName() + message = "\nNo debug messages were logged for %s."% \ + thread.getName() return message def delThreadDebugLog(self, thread): @@ -474,9 +481,9 @@ class UIBase(object): del self.debugmessages[thread] def getThreadExceptionString(self, thread): - message = u"Thread '%s' terminated with exception:\n%s"% \ - (thread.getName(), thread.exit_stacktrace) - message += u"\n" + self.getThreadDebugLog(thread) + message = "Thread '%s' terminated with exception:\n%s"% \ + (thread.getName(), thread.exit_stacktrace) + message += "\n" + self.getThreadDebugLog(thread) return message def threadException(self, thread): @@ -492,21 +499,21 @@ class UIBase(object): #print any exceptions that have occurred over the run if not self.exc_queue.empty(): - self.warn(u"ERROR: Exceptions occurred during the run!") + self.warn("ERROR: Exceptions occurred during the run!") while not self.exc_queue.empty(): msg, exc, exc_traceback = self.exc_queue.get() if msg: - self.warn(u"ERROR: %s\n %s"% (msg, exc)) + self.warn("ERROR: %s\n %s"% (msg, exc)) else: - self.warn(u"ERROR: %s"% (exc)) + self.warn("ERROR: %s"% (exc)) if exc_traceback: - self.warn(u"\nTraceback:\n%s"% "".join( + self.warn("\nTraceback:\n%s"% "".join( traceback.format_tb(exc_traceback))) if errormsg and errortitle: - self.warn(u'ERROR: %s\n\n%s\n'% (errortitle, errormsg)) + self.warn('ERROR: %s\n\n%s\n'% (errortitle, errormsg)) elif errormsg: - self.warn(u'%s\n' % errormsg) + self.warn('%s\n'% errormsg) sys.exit(exitstatus) def threadExited(self, thread):