Singleton Pattern for Config Classes

Overview

The Singleton utility in nshconfig provides a robust, type-safe, and thread-safe way to manage singleton instances of configuration classes. It is designed for scenarios where a single, globally accessible configuration instance is required throughout the lifetime of an application or process.

This feature is implemented as a generic descriptor, making it easy to attach to any config class and ensuring correct usage even in complex inheritance hierarchies.

Key Features

  • Type-Safe: Uses generics and type hints for safe, explicit usage.

  • Thread-Safe: Initialization is protected by a reentrant lock.

  • Descriptor-Based: Attaches to config classes as a class variable, automatically binding to the correct owner.

  • Inheritance-Aware: Prevents accidental sharing of singletons between unrelated subclasses unless explicitly intended.

  • Explicit Reset: Allows resetting the singleton for testability and re-initialization.

Usage

Defining a Singleton on a Config Class

from typing import ClassVar, Self
from nshconfig import Config, Singleton

class MyConfig(Config):
    singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    value: str
    number: int = 0

Initializing and Accessing the Singleton

# Initialize with keyword arguments (creates a new instance)
config = MyConfig.singleton.initialize(value="foo", number=42)

# Or initialize with an existing instance
instance = MyConfig(value="bar")
config = MyConfig.singleton.initialize(instance)

# Access the singleton instance
same_config = MyConfig.singleton.instance()
assert same_config is config

# Try to get the instance if it exists (returns None if not initialized)
maybe_config = MyConfig.singleton.try_instance()

Resetting the Singleton

# Reset the singleton (useful for tests or re-initialization)
MyConfig.singleton.reset()
assert MyConfig.singleton.try_instance() is None

Thread Safety

Initialization is thread-safe. If multiple threads attempt to initialize the singleton simultaneously, only one instance will be created; others will receive a warning and the original instance.

Inheritance Behavior

  • Each subclass must define its own singleton if it wants a separate instance.

  • If a subclass does not define its own singleton, attempts to access it will raise a TypeError.

  • To explicitly share a singleton with a base class, assign it directly:

    class BaseConfig(Config):
        singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    
    class DerivedConfig(BaseConfig):
        singleton: ClassVar[Singleton[BaseConfig]] = BaseConfig.singleton  # Explicitly share
    

API Reference

Singleton

class Singleton(Generic[T]):
    def initialize(self, instance: T) -> T
    def initialize(self, **kwargs: Any) -> T
    def instance(self) -> T
    def try_instance(self) -> T | None
    def reset(self) -> None
  • initialize(...): Initializes the singleton. Can be called with an existing instance or with keyword arguments to construct a new one. If already initialized, returns the existing instance and emits a warning.

  • instance(): Returns the singleton instance. Raises RuntimeError if not initialized.

  • try_instance(): Returns the singleton instance if initialized, else None.

  • reset(): Resets the singleton, allowing re-initialization.

Example: Multiple Singletons

class ConfigA(Config):
    singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    foo: int

class ConfigB(Config):
    singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    bar: str

a = ConfigA.singleton.initialize(foo=1)
b = ConfigB.singleton.initialize(bar="hello")
assert a is not b

Example: Inheritance

class Parent(Config):
    singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    parent_value: str

class Child(Parent):
    singleton: ClassVar[Singleton[Self]] = Singleton[Self]()
    child_value: str

parent = Parent.singleton.initialize(parent_value="p")
child = Child.singleton.initialize(parent_value="c", child_value="c2")
assert Parent.singleton.instance() is not Child.singleton.instance()

Error Handling

  • Accessing instance() before initialization raises RuntimeError.

  • Calling initialize() with no arguments raises TypeError.

  • Accessing a singleton from a subclass that does not define or explicitly assign it raises TypeError.

Testing

See test_singleton.py for comprehensive test coverage, including thread safety, inheritance, and reset behavior.