Last active
          October 22, 2025 17:45 
        
      - 
      
 - 
        
Save outofmbufs/0c6bcab45c14f4cf83895ea114f97449 to your computer and use it in GitHub Desktop.  
    Yet Another python singletons implementation
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | # MIT License | |
| # | |
| # Copyright (c) 2025 Neil Webber | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |
| # of this software and associated documentation files (the "Software"), to deal | |
| # in the Software without restriction, including without limitation the rights | |
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| # copies of the Software, and to permit persons to whom the Software is | |
| # furnished to do so, subject to the following conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included | |
| # in all copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| # SOFTWARE. | |
| from threading import Lock | |
| # SUPPORT FOR MAKING THREAD-SAFE SINGLETON CLASSES | |
| # | |
| # Arguably all this generalized code is overly complicated versus simply | |
| # making the application-specific class enforce singleton-ness itself. | |
| # On top of that, there are some fundamental limitations involving | |
| # subclassing and class-level keyword arguments for __init_subclass__() | |
| # Viewer/programmer discretion advised. | |
| # | |
| # Three ways are provided to make thread-safe singleton classes: | |
| # | |
| # * SingletonMeta -- use as a metaclass | |
| # * Singleton -- subclass this | |
| # * singleton -- use as a class decorator | |
| # | |
| # METACLASS technique: | |
| # | |
| # Specify SingletonMeta as a metaclass. Example: | |
| # | |
| # from singletons import SingletonMeta | |
| # class Foo(metaclass=SingletonMeta): | |
| # pass | |
| # | |
| # f1 = Foo() | |
| # f2 = Foo() | |
| # | |
| # f1 is f2 --> True | |
| # | |
| # SUBCLASS technique: | |
| # | |
| # Under the covers this is the same as specifying SingletonMeta as | |
| # a metaclass, but hides that voodoo: | |
| # | |
| # from singletons import Singleton | |
| # class Foo(Singleton) | |
| # ... behaves the same as the METACLASS example above | |
| # | |
| # DECORATOR technique: | |
| # | |
| # Even more hiding of details, this hides the subclassing and the metaclass: | |
| # | |
| # from singletons import singleton | |
| # @singleton | |
| # class Foo: | |
| # pass | |
| # | |
| # --------------- | |
| # ABOUT PARAMETER CHECKING ON 2nd, 3rd, ... (non)INSTANTIATIONS: | |
| # | |
| # Because subsequent Foo() invocations do not create a new object, | |
| # no underlying __new__ or __init__ calls are made. Consequently, | |
| # there is no argument checking on those calls. Thus: | |
| # | |
| # class Bar(metaclass=SingletonMeta): | |
| # def __init__(self, x): | |
| # self.x = x | |
| # | |
| # b = Bar(1, 2) | |
| # b = Bar() | |
| # | |
| # These calls will both fail because Bar initialization takes one argument. | |
| # However, after a successful instantiation: | |
| # | |
| # b = Bar(12) | |
| # | |
| # subsequent malformed calls will SUCCEED: | |
| # | |
| # b1 = Bar(1, 2) | |
| # b2 = Bar() | |
| # | |
| # because they simply return the already-instantiated singleton object, and | |
| # do not invoke __new__ or __init__. | |
| # | |
| # If required/desired, a class can provide a singleton_validator() method | |
| # with this signature: | |
| # | |
| # # ... validate args/kwargs, raise TypeError if bad | |
| # def singleton_validator(self, *args, **kwargs) | |
| # pass | |
| # | |
| # The method should raise TypeError if the args/kwargs are not acceptable. | |
| # One built-in validator, singleton_sameargs, is supplied that will raise | |
| # a TypeError if the subsequent arguments don't match the originals: | |
| # | |
| # @singleton | |
| # class Foo: | |
| # singleton_validator = SingletonMeta.singleton_sameargs | |
| # def __init__(self, x): | |
| # self.x = x | |
| # | |
| # f1 = Foo(1) | |
| # f2 = Foo(2) --> this will raise TypeError because argument x changed | |
| # | |
| class SingletonMeta(type): | |
| """Metaclass to force any class to be a singleton.""" | |
| # __new__ is invoked when the CLASS using SingletonMeta as a metaclass | |
| # is created. That class will be modified: | |
| # - It gets an attribute, __instance, to record the single object. | |
| # - It gets a lock, __instance_lock, for thread safety. | |
| # | |
| # Note that the dunder name-mangling becomes a _SingletonMeta__ prefix | |
| # in the underlying (not SingletonMeta) cls and so there should not | |
| # be any (realistic) possibility of name conflicts. | |
| def __new__(cls, name, bases, dict_, **kwargs): | |
| newclass = super().__new__(cls, name, bases, dict_, **kwargs) | |
| newclass.__instance = None | |
| newclass.__instance_lock = Lock() | |
| return newclass | |
| # this (no-op) singleton args validator is the default if the class | |
| # does not override it | |
| @staticmethod | |
| def singleton_validator(instance, *args, **kwargs): | |
| """This validator accepts all args/kwargs no matter what.""" | |
| pass | |
| @staticmethod | |
| def singleton_noargs(instance, *args, **kwargs): | |
| """This validator enforces there are no args and no kwargs.""" | |
| if args or kwargs: | |
| raise TypeError( | |
| f"{instance.__class__.__name__}() takes no arguments") | |
| @staticmethod | |
| def singleton_sameargs(instance, *args, **kwargs): | |
| """This validator enforces 'matching' arguments, as defined here.""" | |
| created_args, created_kwargs = instance.__class__.__akw | |
| if args != created_args or kwargs != created_kwargs: | |
| raise TypeError( | |
| f"{instance.__class__.__name__}() arguments mismatch") | |
| # heart of __call__ ... this either returns the singleton if it | |
| # already exists, or causes it to be instantiated. There are two | |
| # return values: the object, and whether it was just made (boolean) | |
| def __create_singleton(klass, *args, **kwargs): | |
| with klass.__instance_lock: | |
| # there is a race outside the lock in __call__, | |
| # so check again here inside the lock. The point of the | |
| # outside the lock test is that it (safely) avoids lock overhead | |
| # in all cases once the singleton has been instantiated. | |
| if klass.__instance is not None: | |
| return klass.__instance, False | |
| # nope, genuinely need to make the instance. | |
| # Note tht args are saved for singleton_sameargs | |
| klass.__akw = (args, kwargs) | |
| klass.__instance = super().__call__(*args, **kwargs) | |
| return klass.__instance, True | |
| # This gets called when the class using SingletonMeta as a metaclass | |
| # gets called to create a new class object. Note that 'klass' in the | |
| # code below is that class, NOT SingletonMeta. The goal here is to | |
| # make the singleton object ONLY if necessary, and either way return it. | |
| def __call__(klass, *args, **kwargs): | |
| # Note: Overhead of locking is substantial; fortunately it can be | |
| # safely avoided in this test for the (common, dominant) case | |
| # of not being the first time(s) through. | |
| if klass.__instance is not None: | |
| instance, first = klass.__instance, False | |
| else: | |
| # (probably) have to make it ... see test again inside lock though | |
| instance, first = klass.__create_singleton(*args, **kwargs) | |
| # When the singleton is first instantiated, __new__ or __init__ | |
| # in the class itself (not this metaclass) can validate args/kwargs. | |
| # But when a previously-instantiated singleton is simply returned, | |
| # they don't get that chance. If the class wants to validate the | |
| # args/kwargs in that case too, it must define a singleton_validator | |
| # method, which will be invoked as shown in the code below. | |
| # The singleton_validator should raise TypeError if it does not | |
| # like the args/kwargs given. If no singleton_validator is defined | |
| # the __new__ logic above ensures the null ("everything is ok") | |
| # validator is set up (so there is always a singleton_validator) | |
| if not first: | |
| klass.singleton_validator(instance, *args, **kwargs) | |
| return instance | |
| # This is how any class can be made into a singleton, by invoking | |
| # the _SingletonMeta as its metaclass. The point of doing that here | |
| # with this stub class is other classes can just derive from Singleton, | |
| # rather than having to say "metaclass=_SingletonMeta). | |
| class Singleton(metaclass=SingletonMeta): | |
| pass | |
| # This is a decorator uses SingletonMeta as the metaclass (instead of type) | |
| # Usage: | |
| # @singleton | |
| # class Foo: | |
| # pass | |
| # | |
| # CAUTION: This creates the decorated class twice - which could have | |
| # implications. In particular, the second creation of the class | |
| # has no way to know what __init_subclass__ style kwargs the | |
| # original class might have used, and passes no such args when | |
| # re-creating the class (to have a SingletonMeta metaclass). | |
| # | |
| # IF __init_subclass__ style class-declaration kwargs are being used, | |
| # do not use the decorator; use explicit subclassing or metaclass. | |
| # | |
| def singleton(klass): | |
| return SingletonMeta(klass.__name__, (klass,), {}) | |
| # | |
| # TESTS | |
| # | |
| if __name__ == "__main__": | |
| import unittest | |
| import threading | |
| from contextlib import nullcontext | |
| class TestMethods(unittest.TestCase): | |
| # METACLASS test: taken directly from comments at top of file | |
| def test_metaclass(self): | |
| class Foo(metaclass=SingletonMeta): | |
| pass | |
| f1 = Foo() | |
| f2 = Foo() | |
| self.assertTrue(f1 is not None) | |
| self.assertTrue(f1 is f2) | |
| # SUBCLASS test: inspired by comments at top of file | |
| def test_subclass(self): | |
| class Foo(Singleton): | |
| pass | |
| f1 = Foo() | |
| f2 = Foo() | |
| self.assertTrue(f1 is not None) | |
| self.assertTrue(f1 is f2) | |
| # DECORATOR test: inspired by comments at top of file | |
| def test_decorator(self): | |
| @singleton | |
| class Foo: | |
| pass | |
| f1 = Foo() | |
| f2 = Foo() | |
| self.assertTrue(f1 is not None) | |
| self.assertTrue(f1 is f2) | |
| # subclassING test, using SingletonMeta metaclass | |
| def test_subclassing1(self): | |
| class Foo(metaclass=SingletonMeta): | |
| pass | |
| class Bar(Foo): | |
| pass | |
| f1 = Foo() | |
| f2 = Foo() | |
| self.assertTrue(f1 is not None) | |
| self.assertTrue(f1 is f2) | |
| b1 = Bar() | |
| b2 = Bar() | |
| self.assertTrue(b1 is not None) | |
| self.assertTrue(b1 is b2) | |
| self.assertTrue(b1 is not f1) | |
| # subclassING test, using Singleton | |
| def test_subclassing2(self): | |
| class Foo(Singleton): | |
| pass | |
| class Bar(Foo): | |
| pass | |
| f1 = Foo() | |
| f2 = Foo() | |
| self.assertTrue(f1 is not None) | |
| self.assertTrue(f1 is f2) | |
| b1 = Bar() | |
| b2 = Bar() | |
| self.assertTrue(b1 is not None) | |
| self.assertTrue(b1 is b2) | |
| self.assertTrue(b1 is not f1) | |
| # test that class-level keyword args make it into __init_subclass__ | |
| def test_clskw1(self): | |
| class Base: | |
| def __init_subclass__(cls, /, clown, **kwargs): | |
| cls.clown = clown | |
| class SubSB(Singleton, Base, clown='bozo'+'SubSB'): | |
| pass | |
| class SubBS(Base, Singleton, clown='bozo'+'SubBS'): | |
| pass | |
| class SubMC(Base, metaclass=SingletonMeta, clown='bozo'+'SubMC'): | |
| pass | |
| for kls in (SubSB, SubBS, SubMC): | |
| xname = 'bozo'+kls.__name__ | |
| self.assertEqual(kls.clown, xname) | |
| s1 = kls() | |
| s2 = kls() | |
| self.assertEqual(s1.clown, xname) | |
| self.assertEqual(s2.clown, xname) | |
| self.assertTrue(s1 is s2) | |
| # test that tests the locking and proves the racing if no locks | |
| def test_race(self): | |
| def makeX(): | |
| """Create the test class X""" | |
| class X(Singleton): | |
| def __init__(self, id): | |
| myattrname = f"ID_{id}" | |
| for j in range(100): | |
| for i in range(100000): | |
| pass | |
| setattr(X, myattrname, id) | |
| return X | |
| # the guts of the test - start all the threads, wait for them | |
| def _race(xk): | |
| threads = [ | |
| threading.Thread(target=xk, args=(t_id,)) | |
| for t_id in range(100)] | |
| for t in threads: | |
| t.start() | |
| for t in threads: | |
| t.join() | |
| # Step 1: test the test ... monkey patch out the locking | |
| # and see if the test produces race failures. | |
| xk = makeX() | |
| setattr(xk, '_SingletonMeta__instance_lock', nullcontext()) | |
| _race(xk) | |
| # since there was no locking and there were significant delays | |
| # in the __init__ method, multiple threads should have | |
| # been in the __init__ logic and created ID_nnnnn attrs. | |
| ids = [a for a in dir(xk) if a.startswith('ID_')] | |
| # this implies more than one thread instantiated the object | |
| self.assertTrue(len(ids) > 1) | |
| # now the same thing without monkey patching and there | |
| # should only be one ID_ attribute created | |
| xk = makeX() # note NEW class so no patches carried over | |
| _race(xk) | |
| ids = [a for a in dir(xk) if a.startswith('ID_')] | |
| self.assertEqual(len(ids), 1) | |
| unittest.main() | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment