more style consistency

Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
This commit is contained in:
Nicolas Sebrecht 2015-01-14 22:58:25 +01:00
parent 5318af606e
commit 461554b7b1
20 changed files with 343 additions and 297 deletions

View File

@ -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():

View File

@ -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

View File

@ -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. " % \

View File

@ -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',

View File

@ -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])

View File

@ -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):

View File

@ -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']

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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]

View File

@ -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):