Source code for pathex.adts.singleton

import re
import threading
from typing import TypeVar

__doc__ = f"""

Singleton decorator
===================

:Module: ``{__name__}``

.. sectionauthor:: Ernesto Soto Gómez <esto.yinyang@gmail.com>
.. codeauthor:: Ernesto Soto Gómez <esto.yinyang@gmail.com>

---------------------------------------------------------------

This module provides a :func:`~.singleton` decorator. Although it may seem that this has little usefulness in Python (because its inherent dynamic nature), the idea is to diminsh the possibilities in that an error may be committed, and to enforce some memory optimization by using only one instance of the decorated class.

.. include:: ../non_essential_disclamer.txt

.. todo:: development

   .. todo:: Review and finish
"""

__all__ = ['singleton']


T = TypeVar('T')


[docs]def singleton(wrapped_class: type[T]) -> type[T]: """Makes a class singleton Use it as a decorator: .. testsetup:: from pathex.adts.singleton import singleton >>> from dataclasses import dataclass >>> @singleton ... @dataclass(init=False) ... class SingletonClass: ... attribute: int ... ... def __init__(self, attribute: int): ... self.attribute = attribute + 1 Any call to the class will return the same object. The first call will make proper initialization, but subsequent calls will ignore the arguments. It is allowed to call the class without arguments after the first call. Use the class call instead or a previously assigned variable. >>> try: ... SingletonClass() ... except TypeError: ... pass # Right! ... else: ... print('Wrong') >>> instance = SingletonClass(23) >>> assert instance is SingletonClass(44) is SingletonClass() is SingletonClass(11) >>> assert SingletonClass().attribute == instance.attribute == 24 >>> assert not hasattr(SingletonClass(), 'instance') Singleton classes can not be subclassed: >>> try: ... class B(SingletonClass): ... pass ... except TypeError: ... pass # Right! ... else: ... print('Wrong!') The representation of the singleton instance is constructed from the name of the class: >>> assert repr(SingletonClass()) == '<SINGLETON_CLASS>' >>> @singleton ... class _A_Class: ... pass >>> assert repr(_A_Class()) == '<_A_CLASS>' """ instance = None lock = threading.Lock() def firstnew(cls, *init_args, **init_kwargs): nonlocal instance with lock: # just in case the very remote possibility of a race condition if instance is None: if '__old_new__' in cls.__dict__: instance = cls.__old_new__(cls, *init_args, **init_kwargs) else: instance = object.__new__(cls) try: cls.__init__(instance, *init_args, **init_kwargs) except: instance = None # rollback changes raise # All destructions must be done after initializations finished properly, # that is, without unexpected exceptions. cls.__new__ = secondnew cls.__init__ = lambda x, *args, **kwargs: None if '__old_new__' in cls.__dict__: delattr(cls, '__old_new__') return instance def secondnew(cls, *init_args, **init_kwargs): return instance @classmethod def __init_subclass__(cls, /, *args, **kwargs): raise TypeError('Singleton class can not be subclassed') def __hash__(self): return hash(id(self)) def __repr__(self): name = self.__class__.__name__ s = re.sub(r'(?<!^)(?<!_)[A-Z]', r'_\g<0>', name) return f"<{s}>".upper() if '__new__' in wrapped_class.__dict__: wrapped_class.__old_new__ = wrapped_class.__new__ wrapped_class.__new__ = firstnew wrapped_class.__init_subclass__ = __init_subclass__ wrapped_class.__repr__ = __repr__ wrapped_class.__hash__ = __hash__ return wrapped_class