summaryrefslogtreecommitdiffstats
path: root/src/python/lmi/base
diff options
context:
space:
mode:
authorMichal Minar <miminar@redhat.com>2013-07-30 14:24:49 +0200
committerMichal Minar <miminar@redhat.com>2013-07-30 15:23:35 +0200
commit2a2bc8a4e9498024c8a85ce2813e7d0f9c5677a0 (patch)
tree1eaab563e14fd7f11880b540417eb2a879536bb8 /src/python/lmi/base
parent3026b7f6476743d862e6caa7816c56017108ee6d (diff)
downloadopenlmi-providers-2a2bc8a4e9498024c8a85ce2813e7d0f9c5677a0.tar.gz
openlmi-providers-2a2bc8a4e9498024c8a85ce2813e7d0f9c5677a0.tar.xz
openlmi-providers-2a2bc8a4e9498024c8a85ce2813e7d0f9c5677a0.zip
openlmi-python: split python package
Split the openlmi-python package to 2: * openlmi-python-base - lmi namespace - functionality for any OpenLMI related python code - contains packages 'lmi' and 'lmi.base' * openlmi-python-providers - common functionality for OpenLMI providers - contains 'lmi.providers'
Diffstat (limited to 'src/python/lmi/base')
-rw-r--r--src/python/lmi/base/BaseConfiguration.py268
-rw-r--r--src/python/lmi/base/__init__.py24
-rw-r--r--src/python/lmi/base/singletonmixin.py560
3 files changed, 852 insertions, 0 deletions
diff --git a/src/python/lmi/base/BaseConfiguration.py b/src/python/lmi/base/BaseConfiguration.py
new file mode 100644
index 0000000..8790acf
--- /dev/null
+++ b/src/python/lmi/base/BaseConfiguration.py
@@ -0,0 +1,268 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Jan Safranek <jsafrane@redhat.com>
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+"""
+Module for BaseConfiguration class.
+
+BaseConfiguration
+--------------------
+
+.. autoclass:: BaseConfiguration
+ :members:
+
+"""
+
+import ConfigParser
+import logging
+import os
+import socket
+
+from lmi.base.singletonmixin import Singleton
+
+def convert_value(section, option, convert_func, value):
+ """
+ Return result of application of ``convert_func`` on value.
+ If the conversion failes, error is logged and ValueError is raised.
+
+ :param section: (``str``) Section of configuration file. Used for
+ error message.
+ :param option: (``str``) Option of configuration file. Used for
+ error message.
+ :param convert_func: (``type``) Conversion function to apply on passed
+ value.
+ :param value: (``basestring``) Value to convert.
+ """
+ if not isinstance(value, basestring):
+ raise TypeError("value must be a string")
+ try:
+ if convert_func is bool:
+ return value.lower() in ('1', 'y', 'yes', 'on', 'true')
+ if convert_func is str and isinstance(value, unicode):
+ return value.encode('utf-8')
+ if convert_func is unicode and isinstance(value, str):
+ return value.decode('utf-8')
+ return convert_func(value)
+ except ValueError as exc:
+ logging.getLogger(__name__).error(
+ 'failed to convert value of "[%s]%s: %s', section, option,
+ exc)
+ raise
+
+class BaseConfiguration(Singleton):
+ """
+ OpenLMI configuration file. By default, it resides in
+ /etc/openlmi/${provider_prefix}/${provider_prefix}.conf.
+
+ There should be only one instance of this class.
+ """
+
+ CONFIG_DIRECTORY_TEMPLATE = '/etc/openlmi/%(provider_prefix)s/'
+ CONFIG_FILE_PATH_TEMPLATE = \
+ CONFIG_DIRECTORY_TEMPLATE + '%(provider_prefix)s.conf'
+
+ PERSISTENT_PATH_TEMPLATE = '/var/lib/openlmi-%(provider_prefix)s/'
+ SETTINGS_DIR = 'settings/'
+
+ DEFAULT_OPTIONS = {
+ 'Namespace' : 'root/cimv2',
+ 'SystemClassName' : 'Linux_ComputerSystem',
+ # Default logging level
+ "Level" : "ERROR",
+ 'DebugBlivet' : 'false',
+ 'Stderr' : 'false',
+ }
+
+ @classmethod
+ def provider_prefix(cls):
+ """
+ This is responsibility of a subclass.
+
+ :rtype: (``string`) Prefix of providers in lowercase. For example
+ configuration class for storage providers would return "storage".
+
+ Result is used to construct configuration paths.
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def default_options(cls):
+ """ :rtype: (``dict``) Dictionary of default values. """
+ return cls.DEFAULT_OPTIONS
+
+ @classmethod
+ def config_directory(cls):
+ """ Base directory with configuration settings. """
+ return cls.CONFIG_DIRECTORY_TEMPLATE % {
+ 'provider_prefix' : cls.provider_prefix() }
+
+ @classmethod
+ def persistent_path(cls):
+ """ Base directory with persistent settings. """
+ return cls.PERSISTENT_PATH_TEMPLATE % {
+ 'provider_prefix': cls.provider_prefix() }
+
+ @classmethod
+ def config_file_path(cls):
+ """ File path of configuration file. """
+ return cls.CONFIG_FILE_PATH_TEMPLATE % {
+ 'provider_prefix' : cls.provider_prefix() }
+
+ @classmethod
+ def mandatory_sections(cls):
+ """
+ Return list of sections, that must be present in configuration
+ file. If not present, they will be created in memory.
+ """
+ return ['Log', 'CIM']
+
+ def __init__(self):
+ """ Initialize and load a configuration file."""
+ self._listeners = set()
+ self.config = ConfigParser.SafeConfigParser(
+ defaults=self.default_options())
+ self.load()
+
+ def add_listener(self, callback):
+ """
+ Add a callback, which will be called when configuration is updated.
+ The callback will be called with instance of this class as
+ parameter:
+ callback(config)
+ """
+ self._listeners.add(callback)
+
+ def remove_listener(self, callback):
+ """
+ Remove previously registered callback.
+ """
+
+ self._listeners.remove(callback)
+
+ def _call_listeners(self):
+ """
+ Call all listeners that configuration has updated.
+ """
+ for callback in self._listeners:
+ callback(self)
+
+ def load(self):
+ """
+ Load configuration from config file path.
+ The file does not need to exist.
+ """
+ self.config.read(self.config_file_path())
+ for section in self.mandatory_sections():
+ if not self.config.has_section(section):
+ self.config.add_section(section)
+ self._call_listeners()
+
+ @property
+ def namespace(self):
+ """ Return namespace of OpenLMI provider. """
+ return self.config.get('CIM', 'Namespace')
+
+ @property
+ def system_class_name(self):
+ """ Return SystemClassName of OpenLMI provider. """
+ return self.config.get('CIM', 'SystemClassName')
+
+ @property
+ def system_name(self):
+ """ Return SystemName of OpenLMI provider. """
+ return socket.getfqdn()
+
+ @property
+ def logging_level(self):
+ """ Return name of logging level in lower case. """
+ return self.config.get('Log', 'Level').lower()
+
+ @property
+ def stderr(self):
+ """ Return True if logging to stderr is enabled. """
+ return self.config.getboolean('Log', 'Stderr')
+
+ def file_path(self, section, option):
+ """
+ Return absolute file path for requested option.
+ Relative path is converted to absolute one with config's directory
+ as a prefix.
+ """
+ path = self.config.get(section, option)
+ if not os.path.isabs(path):
+ path = os.path.join(self.config_directory(), path)
+ return path
+
+ def get_safe(self, section, option, convert=str, fallback=None,
+ *args, **kwargs):
+ """
+ Get the configuration option value as specified type in a safe way.
+ Value is searched in this order:
+ config_file -> defaults_dict -> fallback
+
+ :param section: (``str``) Section name of option.
+ :param option: (``str``) Option name.
+ :param convert: (``type``) Is a conversion function for obtained
+ value. If the value could not be converted, error message is
+ generated and ``fallback`` is returned. This function is not
+ applied to ``fallback`` value. Supported values are:
+ str, unicode, int ,float, long, bool
+
+ :param fallback: Value returned, when section or option does not
+ exists and no default value is given, or when the obtained value
+ could not be converted by supplied function.
+
+ All the other parameters are passed to the ``SafeConfigParser.get()``
+ method.
+ """
+ if not isinstance(section, basestring):
+ raise TypeError('section must be a string')
+ if not isinstance(option, basestring):
+ raise TypeError("option must be a string")
+ if not convert in (str, unicode, int, float, long, bool):
+ raise ValueError("unsupported type for conversion: %s:",
+ getattr(convert, '__name__', 'unknown'))
+ if ( not self.config.has_option(section, option)
+ and not option.lower() in self.default_options()):
+ logging.getLogger(__name__).warn(
+ 'no option value and no default supplied for "[%s]%s"',
+ section, option)
+ return fallback
+ try:
+ value = self.config.get(section, option, *args, **kwargs)
+ except ConfigParser.Error as exc:
+ logging.getLogger(__name__).error(
+ 'failed to get value of "[%s]%s": %s', section, option,
+ exc)
+ return fallback
+ try:
+ # first try to convert value from config
+ return convert_value(section, option, convert, value)
+ except ValueError as exc:
+ logging.getLogger(__name__).error(
+ 'failed to convert value of "[%s]%s: %s', section, option,
+ exc)
+ # if it failes, try the value from defaults
+ if ( option.lower() in self.default_options()
+ and self.default_options()[option.lower()] != value):
+ try:
+ return convert_value(section, option, convert,
+ self.default_options()[option.lower()])
+ except ValueError:
+ pass # error is already logged, no more options left
+ return fallback
diff --git a/src/python/lmi/base/__init__.py b/src/python/lmi/base/__init__.py
new file mode 100644
index 0000000..c3b443f
--- /dev/null
+++ b/src/python/lmi/base/__init__.py
@@ -0,0 +1,24 @@
+# Software Management Providers
+#
+# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+#
+
+"""
+Common utilities for OpenLMI python projects.
+"""
diff --git a/src/python/lmi/base/singletonmixin.py b/src/python/lmi/base/singletonmixin.py
new file mode 100644
index 0000000..c252676
--- /dev/null
+++ b/src/python/lmi/base/singletonmixin.py
@@ -0,0 +1,560 @@
+#pylint: disable-all
+"""
+A Python Singleton mixin class that makes use of some of the ideas
+found at http://c2.com/cgi/wiki?PythonSingleton. Just inherit
+from it and you have a singleton. No code is required in
+subclasses to create singleton behavior -- inheritance from
+Singleton is all that is needed.
+
+Singleton creation is threadsafe.
+
+USAGE:
+
+Just inherit from Singleton. If you need a constructor, include
+an __init__() method in your class as you usually would. However,
+if your class is S, you instantiate the singleton using S.get_instance()
+instead of S(). Repeated calls to S.get_instance() return the
+originally-created instance.
+
+For example:
+
+class S(Singleton):
+
+ def __init__(self, a, b=1):
+ pass
+
+S1 = S.get_instance(1, b=3)
+
+
+Most of the time, that's all you need to know. However, there are some
+other useful behaviors. Read on for a full description:
+
+1) Getting the singleton:
+
+ S.get_instance()
+
+returns the instance of S. If none exists, it is created.
+
+2) The usual idiom to construct an instance by calling the class, i.e.
+
+ S()
+
+is disabled for the sake of clarity.
+
+For one thing, the S() syntax means instantiation, but get_instance()
+usually does not cause instantiation. So the S() syntax would
+be misleading.
+
+Because of that, if S() were allowed, a programmer who didn't
+happen to notice the inheritance from Singleton (or who
+wasn't fully aware of what a Singleton pattern
+does) might think he was creating a new instance,
+which could lead to very unexpected behavior.
+
+So, overall, it is felt that it is better to make things clearer
+by requiring the call of a class method that is defined in
+Singleton. An attempt to instantiate via S() will result
+in a SingletonException being raised.
+
+3) Use __S.__init__() for instantiation processing,
+since S.get_instance() runs S.__init__(), passing it the args it has received.
+
+If no data needs to be passed in at instantiation time,
+you don't need S.__init__().
+
+4) If S.__init__(.) requires parameters, include them ONLY in the
+first call to S.get_instance(). If subsequent calls have arguments,
+a SingletonException is raised by default.
+
+If you find it more convenient for subsequent calls to be allowed to
+have arguments, but for those argumentsto be ignored, just include
+'ignoreSubsequent = True' in your class definition, i.e.:
+
+ class S(Singleton):
+
+ ignoreSubsequent = True
+
+ def __init__(self, a, b=1):
+ pass
+
+5) For testing, it is sometimes convenient for all existing singleton
+instances to be forgotten, so that new instantiations can occur. For that
+reason, a _forget_all_singletons() function is included. Just call
+
+ _forget_all_singletons()
+
+and it is as if no earlier instantiations have occurred.
+
+6) As an implementation detail, classes that inherit
+from Singleton may not have their own __new__
+methods. To make sure this requirement is followed,
+an exception is raised if a Singleton subclass includ
+es __new__. This happens at subclass instantiation
+time (by means of the MetaSingleton metaclass.
+
+
+By Gary Robinson, grobinson@flyfi.com. No rights reserved --
+placed in the public domain -- which is only reasonable considering
+how much it owes to other people's code and ideas which are in the
+public domain. The idea of using a metaclass came from
+a comment on Gary's blog (see
+http://www.garyrobinson.net/2004/03/python_singleto.html#comments).
+Other improvements came from comments and email from other
+people who saw it online. (See the blog post and comments
+for further credits.)
+
+Not guaranteed to be fit for any particular purpose. Use at your
+own risk.
+"""
+
+import threading
+
+class SingletonException(Exception):
+ """
+ Base exception related to singleton handling.
+ """
+ pass
+
+_ST_SINGLETONS = set()
+_LOCK_FOR_SINGLETONS = threading.RLock()
+# Ensure only one instance of each Singleton class is created. This is not
+# bound to the _LOCK_FOR_SINGLETON_CREATION = threading.RLock() individual
+# Singleton class since we need to ensure that there is only one mutex for each
+# Singleton class, which would require having a lock when setting up the
+# Singleton class, which is what this is anyway. So, when any Singleton is
+# created, we lock this lock and then we don't need to lock it again for that
+# class.
+_LOCK_FOR_SINGLETON_CREATION = threading.RLock()
+
+def _create_singleton_instance(cls, lst_args, dct_kw_args):
+ """
+ Creates singleton instance and stores its class in set.
+ """
+ _LOCK_FOR_SINGLETON_CREATION.acquire()
+ try:
+ if cls._is_instantiated(): # some other thread got here first
+ return
+
+ instance = cls.__new__(cls)
+ try:
+ instance.__init__(*lst_args, **dct_kw_args)
+ except TypeError, exc:
+ if '__init__() takes' in exc.message:
+ raise SingletonException, (
+ 'If the singleton requires __init__ args,'
+ ' supply them on first call to get_instance().')
+ else:
+ raise
+ cls.c_instance = instance
+ _add_singleton(cls)
+ finally:
+ _LOCK_FOR_SINGLETON_CREATION.release()
+
+def _add_singleton(cls):
+ """
+ Adds class to singleton set.
+ """
+ _LOCK_FOR_SINGLETONS.acquire()
+ try:
+ assert cls not in _ST_SINGLETONS
+ _ST_SINGLETONS.add(cls)
+ finally:
+ _LOCK_FOR_SINGLETONS.release()
+
+def _remove_singleton(cls):
+ """
+ Removes class from singleton set.
+ """
+ _LOCK_FOR_SINGLETONS.acquire()
+ try:
+ if cls in _ST_SINGLETONS:
+ _ST_SINGLETONS.remove(cls)
+ finally:
+ _LOCK_FOR_SINGLETONS.release()
+
+def _forget_all_singletons():
+ '''
+ This is useful in tests, since it is hard to know which singletons need
+ to be cleared to make a test work.
+ '''
+ _LOCK_FOR_SINGLETONS.acquire()
+ try:
+ for cls in _ST_SINGLETONS.copy():
+ cls._forget_class_instance_reference_for_testing()
+
+ # Might have created some Singletons in the process of tearing down.
+ # Try one more time - there should be a limit to this.
+ i_num_singletons = len(_ST_SINGLETONS)
+ if len(_ST_SINGLETONS) > 0:
+ for cls in _ST_SINGLETONS.copy():
+ cls._forget_class_instance_reference_for_testing()
+ i_num_singletons -= 1
+ assert i_num_singletons == len(_ST_SINGLETONS), \
+ 'Added a singleton while destroying ' + str(cls)
+ assert len(_ST_SINGLETONS) == 0, _ST_SINGLETONS
+ finally:
+ _LOCK_FOR_SINGLETONS.release()
+
+class MetaSingleton(type):
+ """
+ Metaclass for Singleton base class.
+ """
+ def __new__(mcs, str_name, tup_bases, dct):
+ if dct.has_key('__new__'):
+ raise SingletonException, 'Can not override __new__ in a Singleton'
+ return super(MetaSingleton, mcs).__new__(
+ mcs, str_name, tup_bases, dct)
+
+ def __call__(cls, *lst_args, **dictArgs):
+ raise SingletonException, \
+ 'Singletons may only be instantiated through get_instance()'
+
+class Singleton(object):
+ """
+ Base class for all singletons.
+ """
+ __metaclass__ = MetaSingleton
+
+ def get_instance(cls, *lst_args, **dct_kw_args):
+ """
+ Call this to instantiate an instance or retrieve the existing instance.
+ If the singleton requires args to be instantiated, include them the first
+ time you call get_instance.
+ """
+ if cls._is_instantiated():
+ if ( (lst_args or dct_kw_args)
+ and not hasattr(cls, 'ignoreSubsequent')):
+ raise SingletonException, (
+ 'Singleton already instantiated, but get_instance()'
+ ' called with args.')
+ else:
+ _create_singleton_instance(cls, lst_args, dct_kw_args)
+
+ return cls.c_instance #pylint: disable=E1101
+ get_instance = classmethod(get_instance)
+
+ def _is_instantiated(cls):
+ """
+ Don't use hasattr(cls, 'c_instance'), because that screws things
+ up if there is a singleton that extends another singleton.
+ hasattr looks in the base class if it doesn't find in subclass.
+ """
+ return 'c_instance' in cls.__dict__
+ _is_instantiated = classmethod(_is_instantiated)
+
+ # This can be handy for public use also
+ isInstantiated = _is_instantiated
+
+ def _forget_class_instance_reference_for_testing(cls):
+ """
+ This is designed for convenience in testing -- sometimes you
+ want to get rid of a singleton during test code to see what
+ happens when you call get_instance() under a new situation.
+
+ To really delete the object, all external references to it
+ also need to be deleted.
+ """
+ try:
+ if hasattr(cls.c_instance, '_prepare_to_forget_singleton'):
+ # tell instance to release anything it might be holding onto.
+ cls.c_instance._prepare_to_forget_singleton()
+ del cls.c_instance
+ _remove_singleton(cls)
+ except AttributeError:
+ # run up the chain of base classes until we find the one that has
+ # the instance and then delete it there
+ for base_class in cls.__bases__:
+ if issubclass(base_class, Singleton):
+ base_class._forget_class_instance_reference_for_testing()
+ _forget_class_instance_reference_for_testing = classmethod(
+ _forget_class_instance_reference_for_testing)
+
+
+if __name__ == '__main__':
+
+ import unittest
+ import time
+
+ class SingletonMixinPublicTestCase(unittest.TestCase):
+ """
+ TestCase for singleton class.
+ """
+ def testReturnsSameObject(self): #pylint: disable=C0103
+ """
+ Demonstrates normal use -- just call get_instance and it returns a singleton instance
+ """
+
+ class Foo(Singleton):
+ """Singleton child class."""
+ def __init__(self):
+ super(Foo, self).__init__()
+
+ a1 = Foo.get_instance()
+ a2 = Foo.get_instance()
+ self.assertEquals(id(a1), id(a2))
+
+ def testInstantiateWithMultiArgConstructor(self):#pylint: disable=C0103
+ """
+ If the singleton needs args to construct, include them in the first
+ call to get instances.
+ """
+
+ class Bar(Singleton):
+ """Singleton child class."""
+
+ def __init__(self, arg1, arg2):
+ super(Bar, self).__init__()
+ self.arg1 = arg1
+ self.arg2 = arg2
+
+ b1 = Bar.get_instance('arg1 value', 'arg2 value')
+ b2 = Bar.get_instance()
+ self.assertEquals(b1.arg1, 'arg1 value')
+ self.assertEquals(b1.arg2, 'arg2 value')
+ self.assertEquals(id(b1), id(b2))
+
+ def testInstantiateWithKeywordArg(self):
+ """
+ Test instantiation with keyword arguments.
+ """
+
+ class Baz(Singleton):
+ """Singleton child class."""
+ def __init__(self, arg1=5):
+ super(Baz, self).__init__()
+ self.arg1 = arg1
+
+ b1 = Baz.get_instance('arg1 value')
+ b2 = Baz.get_instance()
+ self.assertEquals(b1.arg1, 'arg1 value')
+ self.assertEquals(id(b1), id(b2))
+
+ def testTryToInstantiateWithoutNeededArgs(self):
+ """
+ This tests, improper instantiation.
+ """
+
+ class Foo(Singleton):
+ """Singleton child class."""
+ def __init__(self, arg1, arg2):
+ super(Foo, self).__init__()
+ self.arg1 = arg1
+ self.arg2 = arg2
+
+ self.assertRaises(SingletonException, Foo.get_instance)
+
+ def testPassTypeErrorIfAllArgsThere(self):
+ """
+ Make sure the test for capturing missing args doesn't interfere
+ with a normal TypeError.
+ """
+ class Bar(Singleton):
+ """Singleton child class."""
+ def __init__(self, arg1, arg2):
+ super(Bar, self).__init__()
+ self.arg1 = arg1
+ self.arg2 = arg2
+ raise TypeError, 'some type error'
+
+ self.assertRaises(TypeError, Bar.get_instance, 1, 2)
+
+ def testTryToInstantiateWithoutGetInstance(self):
+ """
+ Demonstrates that singletons can ONLY be instantiated through
+ get_instance, as long as they call Singleton.__init__ during
+ construction.
+
+ If this check is not required, you don't need to call
+ Singleton.__init__().
+ """
+
+ class A(Singleton):
+ def __init__(self):
+ super(A, self).__init__()
+
+ self.assertRaises(SingletonException, A)
+
+ def testDontAllowNew(self):
+
+ def instantiatedAnIllegalClass():
+ class A(Singleton):
+ def __init__(self):
+ super(A, self).__init__()
+
+ def __new__(metaclass, str_name, tup_bases, dct):
+ return super(MetaSingleton, metaclass).__new__(
+ metaclass, str_name, tup_bases, dct)
+
+ self.assertRaises(SingletonException, instantiatedAnIllegalClass)
+
+
+ def testDontAllowArgsAfterConstruction(self):
+ class B(Singleton):
+
+ def __init__(self, arg1, arg2):
+ super(B, self).__init__()
+ self.arg1 = arg1
+ self.arg2 = arg2
+
+ B.get_instance('arg1 value', 'arg2 value')
+ self.assertRaises(SingletonException, B, 'arg1 value', 'arg2 value')
+
+ def test_forgetClassInstanceReferenceForTesting(self):
+ class A(Singleton):
+ def __init__(self):
+ super(A, self).__init__()
+ class B(A):
+ def __init__(self):
+ super(B, self).__init__()
+
+ # check that changing the class after forgetting the instance
+ # produces an instance of the new class
+ a = A.get_instance()
+ assert a.__class__.__name__ == 'A'
+ A._forget_class_instance_reference_for_testing()
+ b = B.get_instance()
+ assert b.__class__.__name__ == 'B'
+
+ # check that invoking the 'forget' on a subclass still deletes
+ # the instance
+ B._forget_class_instance_reference_for_testing()
+ a = A.get_instance()
+ B._forget_class_instance_reference_for_testing()
+ b = B.get_instance()
+ assert b.__class__.__name__ == 'B'
+
+ def test_forgetAllSingletons(self):
+ # Should work if there are no singletons
+ _forget_all_singletons()
+
+ class A(Singleton):
+ ciInitCount = 0
+ def __init__(self):
+ super(A, self).__init__()
+ A.ciInitCount += 1
+
+ A.get_instance()
+ self.assertEqual(A.ciInitCount, 1)
+
+ A.get_instance()
+ self.assertEqual(A.ciInitCount, 1)
+
+ _forget_all_singletons()
+ A.get_instance()
+ self.assertEqual(A.ciInitCount, 2)
+
+ def test_threadedCreation(self):
+ # Check that only one Singleton is created even if multiple threads
+ # try at the same time. If fails, would see assert in _add_singleton
+ class Test_Singleton(Singleton):
+ def __init__(self):
+ super(Test_Singleton, self).__init__()
+
+ class Test_SingletonThread(threading.Thread):
+ def __init__(self, fTargetTime):
+ super(Test_SingletonThread, self).__init__()
+ self._fTargetTime = fTargetTime
+ self._eException = None
+
+ def run(self):
+ try:
+ fSleepTime = self._fTargetTime - time.time()
+ if fSleepTime > 0:
+ time.sleep(fSleepTime)
+ Test_Singleton.get_instance()
+ except Exception, exc:
+ self._eException = exc
+
+ fTargetTime = time.time() + 0.1
+ lstThreads = []
+ for _ in xrange(100):
+ t = Test_SingletonThread(fTargetTime)
+ t.start()
+ lstThreads.append(t)
+ eException = None
+ for t in lstThreads:
+ t.join()
+ if t._eException and not eException:
+ eException = t._eException
+ if eException:
+ raise eException
+
+ def testNoInit(self):
+ """
+ Demonstrates use with a class not defining __init__
+ """
+
+ class A(Singleton):
+ pass
+
+ #INTENTIONALLY UNDEFINED:
+ #def __init__(self):
+ # super(A, self).__init__()
+
+ A.get_instance() #Make sure no exception is raised
+
+ def testMultipleGetInstancesWithArgs(self):
+
+ class A(Singleton):
+
+ ignoreSubsequent = True
+
+ def __init__(self, a, b=1):
+ pass
+
+ a1 = A.get_instance(1)
+ # ignores the second call because of ignoreSubsequent
+ a2 = A.get_instance(2)
+
+ class B(Singleton):
+
+ def __init__(self, a, b=1):
+ pass
+
+ b1 = B.get_instance(1)
+ # No ignoreSubsequent included
+ self.assertRaises(SingletonException, B.get_instance, 2)
+
+ class C(Singleton):
+
+ def __init__(self, a=1):
+ pass
+
+ c1 = C.get_instance(a=1)
+ # No ignoreSubsequent included
+ self.assertRaises(SingletonException, C.get_instance, a=2)
+
+ def testInheritance(self):
+ """
+ It's sometimes said that you can't subclass a singleton (see, for instance,
+ http://steve.yegge.googlepages.com/singleton-considered-stupid point e). This
+ test shows that at least rudimentary subclassing works fine for us.
+ """
+
+ class A(Singleton):
+
+ def set_x(self, x):
+ self.x = x
+
+ def setZ(self, z):
+ raise NotImplementedError
+
+ class B(A):
+
+ def set_x(self, x):
+ self.x = -x
+
+ def set_y(self, y):
+ self.y = y
+
+ a = A.get_instance()
+ a.set_x(5)
+ b = B.get_instance()
+ b.set_x(5)
+ b.set_y(50)
+ self.assertEqual((a.x, b.x, b.y), (5, -5, 50))
+ self.assertRaises(AttributeError, eval, 'a.set_y', {}, locals())
+ self.assertRaises(NotImplementedError, b.setZ, 500)
+
+ unittest.main()
+