From f4140cbbed13679491d10b7731e638f0701e318c Mon Sep 17 00:00:00 2001 From: Eygene Ryabinkin Date: Tue, 5 Feb 2013 07:49:56 +0400 Subject: [PATCH 1/2] Create global instance of command-line options This eases testing of option values inside the code. This instance is implemented as the read-only copy of the obtained 'options' object, so callers won't be able to modify its contents. Signed-off-by: Eygene Ryabinkin --- docs/doc-src/API.rst | 27 +++++++++++++++++++++++ offlineimap/accounts.py | 3 ++- offlineimap/folder/Base.py | 3 ++- offlineimap/globals.py | 12 ++++++++++ offlineimap/init.py | 5 ++--- offlineimap/utils/const.py | 40 +++++++++++++++++++++++++++++++++ test/tests/test_00_globals.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 offlineimap/globals.py create mode 100644 offlineimap/utils/const.py create mode 100755 test/tests/test_00_globals.py diff --git a/docs/doc-src/API.rst b/docs/doc-src/API.rst index 38df996..d3c80bf 100644 --- a/docs/doc-src/API.rst +++ b/docs/doc-src/API.rst @@ -60,3 +60,30 @@ An :class:`accounts.Account` connects two email repositories that are to be sync This execption inherits directly from :exc:`Exception` and is raised on errors during the offlineimap execution. It has an attribute `severity` that denotes the severity level of the error. + + +:mod:`offlineimap.globals` -- module with global variables +========================================================== + +.. module:: offlineimap.globals + +Module :mod:`offlineimap.globals` provides the read-only storage +for the global variables. + +All exported module attributes can be set manually, but this practice +is highly discouraged and shouldn't be used. +However, attributes of all stored variables can only be read, write +access to them is denied. + +Currently, we have only :attr:`options` attribute that holds +command-line options as returned by OptionParser. +The value of :attr:`options` must be set by :func:`set_options` +prior to its first use. + +.. automodule:: offlineimap.globals + :members: + + .. data:: options + + You can access the values of stored options using the usual + syntax, offlineimap.globals.options. diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 88d62d8..c78d7d8 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -15,6 +15,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from offlineimap import mbnames, CustomConfig, OfflineImapError +from offlineimap import globals from offlineimap.repository import Repository from offlineimap.ui import getglobalui from offlineimap.threadutil import InstanceLimitedThread @@ -321,7 +322,7 @@ class SyncableAccount(Account): self.ui.debug('', "Not syncing filtered folder '%s'" "[%s]" % (localfolder, localfolder.repository)) continue # Ignore filtered folder - if self.config.get('general', 'single-thread') == 'False': + if not globals.options.singlethreading: thread = InstanceLimitedThread(\ instancename = 'FOLDER_' + self.remoterepos.getname(), target = syncfolder, diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index e330086..bf9a3a1 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -16,6 +16,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from offlineimap import threadutil +from offlineimap import globals from offlineimap.ui import getglobalui from offlineimap.error import OfflineImapError import offlineimap.accounts @@ -403,7 +404,7 @@ class BaseFolder(object): break self.ui.copyingmessage(uid, num+1, num_to_copy, self, dstfolder) # exceptions are caught in copymessageto() - if self.suggeststhreads() and self.config.get('general', 'single-thread') == 'False': + if self.suggeststhreads() and not globals.options.singlethreading: self.waitforthread() thread = threadutil.InstanceLimitedThread(\ self.getcopyinstancelimit(), diff --git a/offlineimap/globals.py b/offlineimap/globals.py new file mode 100644 index 0000000..b4253f9 --- /dev/null +++ b/offlineimap/globals.py @@ -0,0 +1,12 @@ +# Copyright 2013 Eygene A. Ryabinkin. +# +# Module that holds various global objects. + +from offlineimap.utils import const + +# Holds command-line options for OfflineIMAP. +options = const.ConstProxy() + +def set_options (source): + """ Sets the source for options variable """ + options.set_source (source) diff --git a/offlineimap/init.py b/offlineimap/init.py index d25d7fa..d52fd3a 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -25,6 +25,7 @@ import logging from optparse import OptionParser import offlineimap from offlineimap import accounts, threadutil, syncmaster +from offlineimap import globals from offlineimap.error import OfflineImapError from offlineimap.ui import UI_LIST, setglobalui, getglobalui from offlineimap.CustomConfig import CustomConfigParser @@ -161,6 +162,7 @@ class OfflineImap: ", ".join(UI_LIST.keys())) (options, args) = parser.parse_args() + globals.set_options (options) #read in configuration file configfilename = os.path.expanduser(options.configfile) @@ -251,9 +253,6 @@ class OfflineImap: if type.lower() == 'imap': imaplib.Debug = 5 - # XXX: can we avoid introducing fake configuration item? - config.set_if_not_exists('general', 'single-thread', 'True' if options.singlethreading else 'False') - if options.runonce: # FIXME: maybe need a better for section in accounts.getaccountlist(config): diff --git a/offlineimap/utils/const.py b/offlineimap/utils/const.py new file mode 100644 index 0000000..a62b6a6 --- /dev/null +++ b/offlineimap/utils/const.py @@ -0,0 +1,40 @@ +# Copyright 2013 Eygene A. Ryabinkin. +# +# Collection of classes that implement const-like behaviour +# for various objects. + +import copy + +class ConstProxy (object): + """ + Implements read-only access to a given object + that can be attached to each instance only once. + + """ + + def __init__ (self): + self.__dict__['__source'] = None + + + def __getattr__ (self, name): + src = self.__dict__['__source'] + if src == None: + raise ValueError ("using non-initialized ConstProxy() object") + return copy.deepcopy (getattr (src, name)) + + + def __setattr__ (self, name, value): + raise AttributeError ("tried to set '%s' to '%s' for constant object" % \ + (name, value)) + + + def __delattr__ (self, name): + raise RuntimeError ("tried to delete field '%s' from constant object" % \ + (name)) + + + def set_source (self, source): + """ Sets source object for this instance. """ + if (self.__dict__['__source'] != None): + raise ValueError ("source object is already set") + self.__dict__['__source'] = source diff --git a/test/tests/test_00_globals.py b/test/tests/test_00_globals.py new file mode 100755 index 0000000..b4572f9 --- /dev/null +++ b/test/tests/test_00_globals.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# Copyright 2013 Eygene A. Ryabinkin + +from offlineimap import globals +import unittest + +class Opt: + def __init__(self): + self.one = "baz" + self.two = 42 + self.three = True + + +class TestOfflineimapGlobals(unittest.TestCase): + + @classmethod + def setUpClass(klass): + klass.o = Opt() + globals.set_options (klass.o) + + def test_initial_state(self): + for k in self.o.__dict__.keys(): + self.assertTrue(getattr(self.o, k) == + getattr(globals.options, k)) + + def test_object_changes(self): + self.o.one = "one" + self.o.two = 119 + self.o.three = False + return self.test_initial_state() + + def test_modification(self): + with self.assertRaises(AttributeError): + globals.options.two = True + + def test_deletion(self): + with self.assertRaises(RuntimeError): + del globals.options.three + + def test_nonexistent_key(self): + with self.assertRaises(AttributeError): + a = globals.options.nosuchoption + + def test_double_init(self): + with self.assertRaises(ValueError): + globals.set_options (True) + + +if __name__ == "__main__": + suite = unittest.TestLoader().loadTestsFromTestCase(TestOfflineimapGlobals) + unittest.TextTestRunner(verbosity=2).run(suite) -- 1.8.1