Service Registry

Overview

The ServiceRegistry provides centralized service and widget management for pyqt-reactive applications. Services register themselves at creation time, making them available throughout the application without manual tracking or circular dependencies.

Module: pyqt_reactive.services.service_registry

Core Concepts

Service Registration

Services register themselves via the AutoRegisterServiceMixin:

from pyqt_reactive.services import ServiceRegistry, AutoRegisterServiceMixin

class PlateManagerWidget(QWidget, AutoRegisterServiceMixin):
    """Widget that auto-registers with ServiceRegistry."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # AutoRegisterServiceMixin registers this instance automatically

Service Resolution

Services are retrieved by type:

from pyqt_reactive.services import ServiceRegistry
from my_widgets import PlateManagerWidget

# Get plate manager widget
plate_manager = ServiceRegistry.get(PlateManagerWidget)

if plate_manager:
    # Use the widget
    plate_manager.refresh_plate_list()

AutoRegisterServiceMixin

Mixin for automatic service registration.

Usage:

class MyWidget(QWidget, AutoRegisterServiceMixin):
    """Widget that auto-registers with ServiceRegistry."""

    def __init__(self):
        super().__init__()
        # ServiceRegistry.set(MyWidget, self) called automatically

Implementation:

class AutoRegisterServiceMixin:
    """Mixin to auto-register widgets with ServiceRegistry."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ServiceRegistry.set(type(self), self)

ServiceRegistry API

Register Services

from pyqt_reactive.services import ServiceRegistry

# Register a service instance
ServiceRegistry.set(MyService, my_service_instance)

# Register with explicit service key
ServiceRegistry.set("my_service_key", my_service_instance)

Retrieve Services

# Get by type (preferred)
plate_manager = ServiceRegistry.get(PlateManagerWidget)

# Get by key
service = ServiceRegistry.get("my_service_key")

# Get with default fallback
manager = ServiceRegistry.get(PlateManagerWidget, None)

Check Registration

# Check if service exists
if ServiceRegistry.has(PlateManagerWidget):
    # Use it
    pass

# Check with key
if ServiceRegistry.has("my_service"):
    pass

Clear Services

# Clear a specific service
ServiceRegistry.clear(MyService)

# Clear all services (use with caution)
ServiceRegistry.clear_all()

Common Use Cases

Widget-to-Widget Communication

Previously required traversing widget trees or maintaining manual references:

# Before: Traversal through all windows
for widget in QApplication.topLevelWidgets():
    if hasattr(widget, 'plate_manager'):
        plate_manager = widget.plate_manager
        break

Now use ServiceRegistry:

# After: Direct lookup
from pyqt_reactive.services import ServiceRegistry
from my_widgets import PlateManagerWidget

plate_manager = ServiceRegistry.get(PlateManagerWidget)
if plate_manager:
    # Connect signals
    self.plate_selected.connect(plate_manager.set_current_plate)

Window Handler Registration

Handlers access widgets from ServiceRegistry:

def create_plate_config_window(scope_id: str, object_state=None):
    from pyqt_reactive.services import ServiceRegistry
    from my_widgets import PlateManagerWidget

    # Get plate manager from ServiceRegistry
    plate_manager = ServiceRegistry.get(PlateManagerWidget)
    if not plate_manager:
        logger.warning("Could not find PlateManager for plate config window")
        return None

    orchestrator = ObjectStateRegistry.get_object(scope_id)
    if not orchestrator:
        return None

    window = ConfigWindow(
        config_class=PipelineConfig,
        current_config=orchestrator.pipeline_config,
        scope_id=scope_id,
    )
    window.show()
    return window

Pipeline-to-Plate Manager Connection

Connect pipeline editor to plate manager via ServiceRegistry:

def connect_pipeline_to_plate_manager(pipeline_widget):
    from my_widgets import PlateManagerWidget
    from pyqt_reactive.services import ServiceRegistry

    # Get plate manager from ServiceRegistry
    plate_manager_widget = ServiceRegistry.get(PlateManagerWidget)

    if plate_manager_widget:
        # Connect plate selection signal to pipeline editor
        plate_manager_widget.plate_selected.connect(
            pipeline_widget.set_current_plate
        )

        # Set current plate if already selected
        if plate_manager_widget.selected_plate_path:
            pipeline_widget.set_current_plate(
                plate_manager_widget.selected_plate_path
            )

        logger.debug("Connected pipeline editor to plate manager")
    else:
        logger.warning("Could not find plate manager widget to connect")

Service Lifecycle

Registration Timing

Services are registered at widget creation time:

# In main window
self.plate_manager_widget = PlateManagerWidget(...)
# PlateManagerWidget.__init__ calls ServiceRegistry.set(PlateManagerWidget, self)

# Plate manager is now available immediately
other_widget.connect_to_plate_manager()

Unregistration

Widgets are unregistered automatically when destroyed (if subclassing QObject):

# ServiceRegistry hooks into object destruction
def on_object_destroyed(self):
    service_type = self._registered_service_type
    ServiceRegistry.clear(service_type)

Singleton Pattern

ServiceRegistry enforces one instance per service type:

# First registration
service1 = MyService()
ServiceRegistry.set(MyService, service1)

# Second registration overwrites first
service2 = MyService()
ServiceRegistry.set(MyService, service2)  # Replaces service1

# Only service2 is available
retrieved = ServiceRegistry.get(MyService)
assert retrieved is service2  # True

Thread Safety

ServiceRegistry is not thread-safe by default. All service operations should occur on the main GUI thread:

# CORRECT: Main thread
def main_thread_function():
    service = ServiceRegistry.get(MyService)
    service.do_something()

# INCORRECT: Background thread
def background_thread_function():
    # This can cause race conditions
    service = ServiceRegistry.get(MyService)
    service.do_something()

If thread-safe access is needed, implement a wrapper:

from threading import Lock

class ThreadSafeServiceRegistry:
    def __init__(self):
        self._services = {}
        self._lock = Lock()

    def get(self, service_type, default=None):
        with self._lock:
            return self._services.get(service_type, default)

    def set(self, service_type, instance):
        with self._lock:
            self._services[service_type] = instance

Best Practices

Use Type-Based Keys

Prefer class types over string keys:

# PREFERRED: Type-based
ServiceRegistry.set(PlateManagerWidget, widget)
manager = ServiceRegistry.get(PlateManagerWidget)

# AVOID: String-based (unless necessary)
ServiceRegistry.set("plate_manager", widget)
manager = ServiceRegistry.get("plate_manager")

Check Before Use

Always check if service exists:

manager = ServiceRegistry.get(PlateManagerWidget)
if manager:
    manager.refresh()
else:
    logger.warning("PlateManager not available")

Auto-Register Widgets

Use AutoRegisterServiceMixin for widgets:

# DO: Auto-register
class PlateManagerWidget(QWidget, AutoRegisterServiceMixin):
    def __init__(self):
        super().__init__()
        # Registered automatically

# AVOID: Manual registration
class PlateManagerWidget(QWidget):
    def __init__(self):
        super().__init__()
        ServiceRegistry.set(PlateManagerWidget, self)  # Boilerplate

Avoid Circular Dependencies

ServiceRegistry breaks dependency chains:

# BEFORE: Circular dependency
# PipelineEditor needs PlateManager
# PlateManager needs PipelineEditor
# Both import each other → circular

# AFTER: ServiceRegistry breaks the cycle
# PipelineEditor imports PlateManagerWidget (type only)
# PlateManagerWidget imports PipelineEditorWidget (type only)
# Both resolve via ServiceRegistry.get() at runtime

Service Keys Design

Design service keys with clear semantics:

# Use concrete widget/service types
ServiceRegistry.set(PlateManagerWidget, widget)

# For multiple instances, use distinct service classes
class MainPlateManagerWidget(PlateManagerWidget): pass
class SecondaryPlateManagerWidget(PlateManagerWidget): pass

ServiceRegistry.set(MainPlateManagerWidget, widget1)
ServiceRegistry.set(SecondaryPlateManagerWidget, widget2)

Integration Points

Window Handlers

Window handlers use ServiceRegistry to access widgets:

def create_step_editor_window(scope_id: str, object_state=None):
    plate_manager = ServiceRegistry.get(PlateManagerWidget)
    orchestrator = ObjectStateRegistry.get_object(plate_path)

    window = DualEditorWindow(
        step_data=step,
        orchestrator=orchestrator,
    )
    return window

Main Window

Main window removes widget tracking:

class MainWindow(QMainWindow):
    def __init__(self):
        # Before: self.floating_windows = {}
        # After: No tracking needed

        self.plate_manager_widget = PlateManagerWidget()
        # Auto-registered, accessible everywhere

See Also