diff --git a/external/ObjectState b/external/ObjectState index b0004daf32..9c31e4b07b 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit b0004daf3223326ac78689ba7b2e68abc18ae298 +Subproject commit 9c31e4b07b5537c0ed2307c5a63034c708b86bbb diff --git a/external/python-introspect b/external/python-introspect index 4e8f8bde5f..6f5ac0b79d 160000 --- a/external/python-introspect +++ b/external/python-introspect @@ -1 +1 @@ -Subproject commit 4e8f8bde5f750d9a50eacac80acd3806123addd8 +Subproject commit 6f5ac0b79d6cef163df6a8389e72a889a91ed548 diff --git a/openhcs/processing/backends/lib_registry/registry_service.py b/openhcs/processing/backends/lib_registry/registry_service.py index 5f1c3d226a..453e1f734a 100644 --- a/openhcs/processing/backends/lib_registry/registry_service.py +++ b/openhcs/processing/backends/lib_registry/registry_service.py @@ -14,9 +14,12 @@ import threading from collections.abc import Callable from concurrent.futures import CancelledError +from dataclasses import is_dataclass from typing import TYPE_CHECKING, Dict, Optional from arraybridge import MemoryType +from objectstate import LazyDataclassFactory +from python_introspect import SignatureAnalyzer from pyqt_reactive.process_launch import BackgroundProcessLaunchPolicy from zmqruntime import OperationCancellation from zmqruntime.client import endpoint_process @@ -161,6 +164,14 @@ def prepare_in_current_process( emit_status( f"Prepared callable {preparation.projection.module_name}.{preparation.projection.name}" ) + emit_status("Preparing registered configuration source declarations") + for declaration in dict.fromkeys( + declaration + for pair in LazyDataclassFactory.registered_type_pairs() + for declaration in pair + if is_dataclass(declaration) + ): + SignatureAnalyzer.prepare_dataclass_declaration(declaration) emit_status(f"Registered kernels ready ({len(callables)} callables)") emit_status(f"Function catalog ready ({len(cls._metadata_cache)} callables)") return cls._metadata_cache diff --git a/tests/unit/test_registered_configuration_source_readiness.py b/tests/unit/test_registered_configuration_source_readiness.py new file mode 100644 index 0000000000..1225e0d429 --- /dev/null +++ b/tests/unit/test_registered_configuration_source_readiness.py @@ -0,0 +1,75 @@ +"""Configuration source preparation follows hooks and precedes registry readiness.""" + +from dataclasses import dataclass, field +from types import SimpleNamespace + +import objectstate.lazy_factory as lazy_factory +from python_introspect import SignatureAnalyzer + +from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch +from openhcs.processing.backends.lib_registry.registry_service import RegistryService + + +def test_registry_prepares_current_configuration_declarations_before_ready(monkeypatch): + events = [] + factories = [] + + def default_value(): + factories.append("evaluated") + return 3 + + @dataclass + class PublicConfig: + value: int = field(default_factory=default_value) + + @dataclass + class LazyConfig: + value: int | None = None + + @dataclass + class LaterPublicConfig: + value: int = field(default_factory=default_value) + + @dataclass + class LaterLazyConfig: + value: int | None = None + + def raw(image): + return image + + metadata = {"raw": SimpleNamespace(func=raw)} + monkeypatch.setattr(RegistryService, "_metadata_cache", metadata) + monkeypatch.setattr(lazy_factory, "_lazy_type_registry", {}) + monkeypatch.setattr(lazy_factory, "_base_to_lazy_registry", {}) + monkeypatch.setattr(PreparationCacheBatch, "populate_child_caches", lambda *a, **k: None) + pairs = iter(((LazyConfig, PublicConfig), (LaterLazyConfig, LaterPublicConfig))) + + def prepare(owner): + events.append("hook") + lazy_factory.register_lazy_type_mapping(*next(pairs)) + + original = SignatureAnalyzer.prepare_dataclass_declaration + + def declaration(config_type): + assert "hook" in events + events.append(config_type) + original(config_type) + + monkeypatch.setattr(CallablePreparation, "prepare", prepare) + monkeypatch.setattr(SignatureAnalyzer, "prepare_dataclass_declaration", declaration) + # The registry admits types that are not dataclasses; readiness filters them. + lazy_factory.register_lazy_type_mapping(dict, list) + assert RegistryService.prepare_in_current_process(status_callback=events.append) is metadata + assert factories == [] + first_ready = next(i for i, item in enumerate(events) if isinstance(item, str) and "kernels ready" in item) + assert events.index(LazyConfig) < first_ready + assert events.index(PublicConfig) < first_ready + assert LaterPublicConfig not in events and dict not in events and list not in events + events.clear() + # An already loaded callable catalog must not freeze the configuration roster. + RegistryService.prepare_in_current_process(status_callback=events.append) + second_ready = next(i for i, item in enumerate(events) if isinstance(item, str) and "kernels ready" in item) + for config_type in (LazyConfig, PublicConfig, LaterLazyConfig, LaterPublicConfig): + assert events.count(config_type) == 1 + assert events.index(config_type) < second_ready + assert factories == []