From 10fdc60131c72ad7a84dfab0e51c2262cd507794 Mon Sep 17 00:00:00 2001 From: Alexandre hassan Date: Fri, 14 Nov 2025 15:43:19 +0000 Subject: [PATCH] add a Singleton metaclass and remove unmaintained pysingleton --- radish/customtyperegistry.py | 5 ++-- radish/extensionregistry.py | 5 ++-- radish/hookregistry.py | 5 ++-- radish/stepregistry.py | 6 ++--- radish/utils.py | 33 +++++++++++++++++++++++++++ requirements.txt | 1 - setup.py | 1 - tests/unit/test_utils.py | 44 ++++++++++++++++++++++++++++++++++++ 8 files changed, 85 insertions(+), 15 deletions(-) diff --git a/radish/customtyperegistry.py b/radish/customtyperegistry.py index 00ccccf4..c9bed493 100644 --- a/radish/customtyperegistry.py +++ b/radish/customtyperegistry.py @@ -4,13 +4,12 @@ # Keep for backwards compat: from parse_type import TypeBuilder # noqa: F401 -from singleton import singleton from .exceptions import RadishError +from .utils import Singleton -@singleton() -class CustomTypeRegistry: +class CustomTypeRegistry(metaclass=Singleton): """ Registry for all custom argument expressions """ diff --git a/radish/extensionregistry.py b/radish/extensionregistry.py index 30ed8f0c..2fb26c74 100644 --- a/radish/extensionregistry.py +++ b/radish/extensionregistry.py @@ -2,11 +2,10 @@ Provide plugin interface for radish extensions """ -from singleton import singleton +from .utils import Singleton -@singleton() -class ExtensionRegistry: +class ExtensionRegistry(metaclass=Singleton): """ Registers all extensions """ diff --git a/radish/hookregistry.py b/radish/hookregistry.py index 15fcdd5e..c774eae2 100644 --- a/radish/hookregistry.py +++ b/radish/hookregistry.py @@ -3,14 +3,13 @@ """ import tagexpressions -from singleton import singleton from . import utils from .exceptions import HookError +from .utils import Singleton -@singleton() -class HookRegistry: +class HookRegistry(metaclass=Singleton): """ Represents an object with all registered hooks """ diff --git a/radish/stepregistry.py b/radish/stepregistry.py index 3ce477bc..b4081e4a 100644 --- a/radish/stepregistry.py +++ b/radish/stepregistry.py @@ -5,13 +5,11 @@ import inspect import re -from singleton import singleton - from .exceptions import RadishError, SameStepError, StepRegexError +from .utils import Singleton -@singleton() -class StepRegistry: +class StepRegistry(metaclass=Singleton): """ Represents the step registry """ diff --git a/radish/utils.py b/radish/utils.py index 4dc50ae6..a9f02273 100644 --- a/radish/utils.py +++ b/radish/utils.py @@ -12,6 +12,7 @@ import traceback import warnings from datetime import datetime, timedelta, timezone +from threading import Lock class Failure: @@ -230,3 +231,35 @@ def split_unescape(s, delim, escape="\\", unescape=True): current.append(ch) ret.append("".join(current)) return ret + + +class Singleton(type): + """ + Metaclass for singleton classes. To create a singleton class use this metaclass: + + class MySingletonClass(metaclass=Singleton): + pass + + Now Singleton will ensure that only one instance of MySingletonClass is created. + """ + + _instances = {} + _lock = Lock() + + def __call__(cls, *args, **kwargs): + """ + Called when you create a new instance of a class with this metaclass. + + It ensures that only one instance of the class is created (singleton pattern). + + When you call `MySingletonClass(args, kwargs)`, python calls + `Singleton.__call__(MySingletonClass, args, kwargs)`. + + This method in turn check if an instance of `MySingletonClass` already exists. + If not, it creates one and stores it in the `_instances` dictionary. + """ + if cls not in cls._instances: # Only grab the lock if the instance is not in the dict + with cls._lock: # Ensure thread-safety + if cls not in cls._instances: # in case instance was added while waiting for lock + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/requirements.txt b/requirements.txt index 09c35ec9..f460a6b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -pysingleton==0.2.1 colorful==0.5.8 docopt==0.6.2 ipython==7.34.0 diff --git a/setup.py b/setup.py index 63194f22..708ba446 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ def get_meta(name): # mandatory requirements for the radish base features requirements = [ "docopt", - "pysingleton", "colorful>=0.3.11", "tag-expressions>=2.0.0", "parse_type>0.4.0", diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index fee953c0..245afebd 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -8,6 +8,7 @@ """ from datetime import datetime, timezone +from threading import Lock, Thread import pytest from freezegun import freeze_time @@ -67,3 +68,46 @@ def test_make_unique_obj_list(): value_list.sort() assert value_list == ["1", "2"] + + +def test_singleton_behavior(): + """Test that Singleton metaclass enforces singleton behavior""" + + class MySingletonClass(metaclass=utils.Singleton): + def __init__(self): + self.value = 42 + + instance1 = MySingletonClass() + instance1.value = 100 # Modify value to test singleton behavior + instance2 = MySingletonClass() + + assert instance1 is instance2 # They should be the same instance + assert instance1.value == instance2.value # Value should be the same changed value + + +def test_singleton_thread_safety(): + """ + Test that Singleton metaclass is thread-safe by creating multiple instances in parallel threads + """ + + class MyThreadSafeSingleton(metaclass=utils.Singleton): + def __init__(self): + self.value = 0 + + instances = [] + lock = Lock() + + def create_instance(): + instance = MyThreadSafeSingleton() + with lock: # Ensure thread-safe appending + instances.append(instance) + + threads = [Thread(target=create_instance) for _ in range(10)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + first_instance = instances[0] + for instance in instances: + assert instance is first_instance