0

I've created a Singleton using a MetaClass as discussed in Method 3 of this answer

 class Singleton(type):
      _instances = {}
      def __call__(cls, *args, **kwargs):
         if cls not in cls._instances:
             cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
         return cls._instances[cls]


class MySing(metaclass=Singleton): ...

I'd like to be able to clear the Singleton in the setUp() method of a unittest.TestCase so that each test starts with a clean Singleton.

I guess I don't really understand what this metaClass is doing because I can't get the correct incantation for a clear() method:

     def clear(self):
       try:
          del(Singleton._instances[type(self)]
       except KeyError:
          pass   #Sometimes we clear before creating

Any thoughts on what I'm doing wrong here? My singleton is not getting cleared.

sing=MySing()
sing.clear()

The type call above returns Singleton not MySing.

chepner
  • 389,128
  • 51
  • 403
  • 529
Ray Salemi
  • 2,931
  • 1
  • 18
  • 45
  • 2
    Except for the syntax error at `del(Singleton` this code works fine. If you try `print(sing is MySing())` you'll get `False` as output. – Aran-Fey Apr 27 '18 at 15:15
  • 3
    `clear` would make more sense as a class method than an instance method. – chepner Apr 27 '18 at 15:22
  • I found my error. My example here was not what was really in my code. My code actually had `MySing.clear()` which is a class method and explains why `self` was different than I thought. – Ray Salemi Apr 27 '18 at 15:24
  • Do you want `_instances` to be an empty dictionary or do you just want to remove a specific item.? – wwii Apr 27 '18 at 15:25
  • Remove specific instances. – Ray Salemi Apr 27 '18 at 15:25

2 Answers2

5

Let's walk through a (corrected) definition of Singleton and a class defined using it. I'm replacing uses of cls with Singleton where the lookup is passed through anyway.

 class Singleton(type):
     _instances = {}

     # Each of the following functions use cls instead of self
     # to emphasize that although they are instance methods of
     # Singleton, they are also *class* methods of a class defined
     # with Singleton
     def __call__(cls, *args, **kwargs):
         if cls not in Singleton._instances:
             Singleton._instances[cls] = super().__call__(*args, **kwargs)
         return Singleton._instances[cls]

     def clear(cls):
         try:
             del Singleton._instances[cls]
         except KeyError:
             continue

class MySing(metaclass=Singleton):
    pass

s1 = MySing()   # First call: actually creates a new instance
s2 = MySing()   # Second call: returns the cached instance
assert s1 is s2 # Yup, they are the same
MySing.clear()  # Throw away the cached instance
s3 = MySing()   # Third call: no cached instance, so create one
assert s1 is not s3  # Yup, s3 is a distinct new instance

First, _instances is a class attribute of the metaclass, meant to map a class to a unique instance of that class.

__call__ is an instance method of the metaclass; its purpose is to make instances of the metaclass (i.e., classes) callable. cls here is the class being defined, not the metaclass. So each time you call MyClass(), that converts to Singleton.__call__(MyClass).

clear is also a instance method of the metaclass, meaning it also takes a instance of the meta class (i.e again, a class) as an argument (not an instance of the class defined with the metaclass.) This means MyClass.clear() is the same as Singleton.clear(MyClass). (This also means you can, but probably shouldn't for clarity, write s1.clear().)

The identification of metaclass instance methods with "regular" class class methods also explains why you need to use __call__ in the meta class where you would use __new__ in the regular class: __new__ is special-cased as a class method without having to decorate it as such. It's slightly tricky for a metaclass to define an instance method for its instances, so we just use __call__ (since type.__call__ doesn't do much, if anything, beyond invoking the correct __new__ method).

chepner
  • 389,128
  • 51
  • 403
  • 529
-2

I see three useful test cases for this metaclass at first glance.

  • Test whether a single class creation works properly.
  • Test whether no new class is created after the initial one.
  • Test whether multiple classes can use this metaclass in conjunction.

All of these tests can be achieved without a "reset" button. After which you'll have covered most of your bases. (I might have forgotten one).

Simply create a few different TestClasses that use this metaclass and check their Id's and types.

Shine
  • 461
  • 4
  • 12
  • 1
    This really doesn't get to the question at hand. The code being tested uses a singleton run once per invocation, so we're testing that each invocation works properly in each test. That means clearing the Singleton before running each test. – Ray Salemi Apr 27 '18 at 15:52
  • This does, though, point to the fact that there is probably a better way to structure your tests that don't require a `Singleton` metaclass in the first place, such as defining a single instance of a class from `setupModule` instead of "recreating" the object in each test case, to use `unittest` as an example. – chepner Apr 27 '18 at 16:00
  • I would argue a method `.clear()` **available on every instance** that resets your Singleton is a much greater risk than a less-than-perfect test setting. – Shine Apr 27 '18 at 16:12
  • Without providing a solution for unit testing the singleton class, you aren't answering the real first question, which is how to test classes with metaclass=Singleton. Without that, most unit tests will fail because single instance of a class with metaclass=Singleton will have unpredictable state. – Jason Harrison Feb 17 '21 at 19:09