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..95efedfe 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,18 @@ def split_unescape(s, delim, escape="\\", unescape=True): current.append(ch) ret.append("".join(current)) return ret + + +class Singleton(type): + """ + Metaclass for singleton classes + """ + + _instances = {} + _lock = Lock() + + def __call__(cls, *args, **kwargs): + with cls._lock: + if cls not in cls._instances: + 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..e1b1603a 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,44 @@ 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 + instance2 = MySingletonClass() + + assert instance1 is instance2 + assert instance1.value == instance2.value + + +def test_singleton_thread_safety(): + """Test that Singleton metaclass is thread-safe""" + + 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