#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright 2011 Google Inc. All Rights Reserved.

"""Thread-safe implementation of a singleton decorator.

Sometimes, only a single instance of a class should ever be created. This file
provides a wrapper that turns a class into a Singleton. The constructor may
only be called once; a static method Singleton() provides access to the
constructed instance. If Singleton() is called before the constructor, or if
the constructor is called multiple times, Errors are raised. This wrapper is
thread-safe; calls to the constructor and Singleton() method are protected
by per-class locks.

Singletons are often associated with bad coding practices; see
https://wiki/Main/SingletonsConsideredDangerous and decide if you should
really be using this functionality. Consider alternatives, like the
"Borg pattern" where object state (instead of object identity) is shared.

To make your singletons more testable, use the following idiom:


class A(...):
  "All the complicated code goes in here, can be tested normally..."
  ..
  ..


@singleton.Singleton
class B(A):
  "Singleton instance of A"


Example usage:

from google.pyglib import singleton


@singleton.Singleton
class Foo(object):
  "Example singleton"


a = Foo()
b = Foo.Singleton()
c = Foo.Singleton()
assert a == b
assert b == c


Note that class decorator syntax was added after python2.4. If your code
is python2.4 compliant, use an idiom like:

from google.pyglib import singleton


class Foo(object):
  "Foo class"

Bar = singleton.Singleton(Foo)


a = Bar()
b = Bar.Singleton()
c = Bar.Singleton()
assert a == b
assert b == c
"""



import threading

_CLASS_LOCKS = {}  # Holds the per-class locks.
_CLASS_LOCKS_LOCK = threading.Lock()  # Lock for obtaining a per-class lock.
_INSTANCES = {}  # Mapping from class to instantiated object.


class Error(Exception):
  """Base error class."""


class AlreadyHasSingletonMethodError(Error):
  """Raised if the class already defines a Singleton() method."""

  def __init__(self, cls):
    Error.__init__(self)
    self.cls = cls

  def __str__(self):
    return 'Class already has a Singleton() method: %s' % self.cls


class NotConstructedError(Error):
  """Raised if the constructor has not been called yet."""

  def __init__(self, cls):
    Error.__init__(self)
    self.cls = cls

  def __str__(self):
    return 'Constructor has not yet been called for class %s' % self.cls


class ConstructorCalledAgainError(Error):
  """Raised if the constructor is called twice for a singleton."""

  def __init__(self, cls, args, kws):
    Error.__init__(self)
    self.cls = cls
    self.args = args
    self.kws = kws

  def __str__(self):
    return ('Constructor called (again) on class %s with args %s and kws %s'
            % (self.cls, self.args, self.kws))


def _GetClassLock(cls):
  """Returns the lock associated with the class."""
  with _CLASS_LOCKS_LOCK:
    if cls not in _CLASS_LOCKS:
      _CLASS_LOCKS[cls] = threading.Lock()
    return _CLASS_LOCKS[cls]


def Singleton(cls):
  """Turn a class into a singleton.

  One call to the constructor is allowed. After that, all future calls
  must be to the Singleton() static method.

  This code is multithread-safe. Shared locks are held for brief periods
  of time; when a class is instantiated, it uses a class specific lock.

  Args:
    cls: The class to decorate.

  Returns:
    The singleton class. Note that this class is an extension
    of the class it is decorating.

  Raises:
    AlreadyHasSingletonMethodError: If the class has a Singleton method
      defined.
  """
  if hasattr(cls, 'Singleton'):
    raise AlreadyHasSingletonMethodError(cls)

  class _Singleton(cls):

    def __init__(self, *args, **kws):
      class_lock = _GetClassLock(cls)
      with class_lock:
        if cls not in _INSTANCES:
          cls.__init__(self, *args, **kws)
          _INSTANCES[cls] = self
        else:
          raise ConstructorCalledAgainError(cls, args, kws)

    @staticmethod
    def Singleton():
      class_lock = _GetClassLock(cls)
      with class_lock:
        if cls not in _INSTANCES:
          raise NotConstructedError(cls)
        else:
          return _INSTANCES[cls]

  return _Singleton
