diff options
author | Michal Minar <miminar@redhat.com> | 2013-07-30 14:24:49 +0200 |
---|---|---|
committer | Michal Minar <miminar@redhat.com> | 2013-07-30 15:23:35 +0200 |
commit | 2a2bc8a4e9498024c8a85ce2813e7d0f9c5677a0 (patch) | |
tree | 1eaab563e14fd7f11880b540417eb2a879536bb8 /src/python/lmi/base | |
parent | 3026b7f6476743d862e6caa7816c56017108ee6d (diff) | |
download | openlmi-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.py | 268 | ||||
-rw-r--r-- | src/python/lmi/base/__init__.py | 24 | ||||
-rw-r--r-- | src/python/lmi/base/singletonmixin.py | 560 |
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() + |