Files
deepset-ai--haystack/test/core/test_serialization.py
T
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

647 lines
28 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import sys
from copy import deepcopy
from typing import Any
from unittest.mock import Mock
import pytest
from haystack.core.component import component
from haystack.core.errors import DeserializationError, SerializationError
from haystack.core.pipeline import Pipeline
from haystack.core.serialization import (
component_from_dict,
component_to_dict,
default_from_dict,
default_to_dict,
generate_qualified_class_name,
import_class_by_name,
)
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.testing import factory
from haystack.utils import ComponentDevice, Secret
from haystack.utils.auth import EnvVarSecret
from haystack.utils.device import Device, DeviceMap
def test_default_component_to_dict():
MyComponent = factory.component_class("MyComponent")
comp = MyComponent()
res = default_to_dict(comp)
assert res == {"type": "haystack.testing.factory.MyComponent", "init_parameters": {}}
def test_default_component_to_dict_with_init_parameters():
MyComponent = factory.component_class("MyComponent")
comp = MyComponent()
res = default_to_dict(comp, some_key="some_value")
assert res == {"type": "haystack.testing.factory.MyComponent", "init_parameters": {"some_key": "some_value"}}
def test_default_component_from_dict():
def custom_init(self, some_param):
self.some_param = some_param
extra_fields = {"__init__": custom_init}
MyComponent = factory.component_class("MyComponent", extra_fields=extra_fields)
comp = default_from_dict(
MyComponent, {"type": "haystack.testing.factory.MyComponent", "init_parameters": {"some_param": 10}}
)
assert isinstance(comp, MyComponent)
assert comp.some_param == 10 # type: ignore[attr-defined]
def test_default_component_from_dict_without_type():
with pytest.raises(DeserializationError, match="Missing 'type' in serialization data"):
default_from_dict(Mock, {})
def test_default_component_from_dict_unregistered_component(request):
# We use the test function name as component name to make sure it's not registered.
# Since the registry is global we risk to have a component with the same name registered in another test.
component_name = request.node.name
with pytest.raises(DeserializationError, match=f"Class '{component_name}' can't be deserialized as 'Mock'"):
default_from_dict(Mock, {"type": component_name})
def test_from_dict_import_type():
pipeline_dict = {
"metadata": {},
"components": {
"greeter": {
"type": "haystack.testing.sample_components.greet.Greet",
"init_parameters": {
"message": "\nGreeting component says: Hi! The value is {value}\n",
"log_level": "INFO",
},
}
},
"connections": [],
}
# remove the target component from the registry if already there
component.registry.pop("haystack.testing.sample_components.greet.Greet", None)
# remove the module from sys.modules if already there
sys.modules.pop("haystack.testing.sample_components.greet", None)
p = Pipeline.from_dict(pipeline_dict)
from haystack.testing.sample_components.greet import Greet
assert type(p.get_component("greeter")) == Greet
def test_get_qualified_class_name():
MyComponent = factory.component_class("MyComponent")
comp = MyComponent()
res = generate_qualified_class_name(type(comp))
assert res == "haystack.testing.factory.MyComponent"
def test_import_class_by_name():
data = "haystack.core.pipeline.Pipeline"
class_object = import_class_by_name(data)
class_instance = class_object()
assert isinstance(class_instance, Pipeline)
def test_import_class_by_name_no_valid_class():
# A name that passes the deserialization allowlist but cannot be resolved should raise ImportError.
data = "haystack.does.not.exist.Class"
with pytest.raises(ImportError):
import_class_by_name(data)
def test_import_class_by_name_rejects_untrusted_module():
# A module outside the default allowlist is rejected before the import is attempted.
with pytest.raises(DeserializationError, match="not on the trusted-module allowlist"):
import_class_by_name("some.invalid.class")
class CustomData:
def __init__(self, a: int, b: str) -> None:
self.a = a
self.b = b
@component()
class UnserializableClass:
def __init__(self, a: int, b: str, c: CustomData) -> None:
self.a = a
self.b = b
self.c = c
def run(self):
pass
def test_component_to_dict_invalid_type():
with pytest.raises(SerializationError, match="unsupported value of type 'CustomData'"):
component_to_dict(UnserializableClass(1, "s", CustomData(99, "aa")), "invalid_component")
@component()
class NestedNonStringKeyClass:
def __init__(self, mapping: dict[Any, Any]) -> None:
self.mapping = mapping
def run(self):
pass
def test_component_to_dict_nested_non_string_key():
# A non-string key nested inside init_parameters must be rejected too, not just at the top level.
with pytest.raises(SerializationError, match="non-string key"):
component_to_dict(NestedNonStringKeyClass(mapping={1: "a"}), "invalid_component")
@component
class CustomComponentWithSecrets:
def __init__(self, api_key: Secret | None = None, token: Secret | None = None, regular_param: str | None = None):
self.api_key = api_key
self.token = token
self.regular_param = regular_param
@component.output_types(value=str)
def run(self, value: str) -> dict[str, str]:
return {"value": value}
def test_component_to_dict_with_secret():
"""Test that Secret instances are automatically serialized in component_to_dict."""
# Test with EnvVarSecret (serializable)
env_secret = Secret.from_env_var("TEST_API_KEY")
comp = CustomComponentWithSecrets(api_key=env_secret)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["api_key"] == env_secret.to_dict()
assert res["init_parameters"]["api_key"]["type"] == "env_var"
# Test with None
comp = CustomComponentWithSecrets(api_key=None)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["api_key"] is None
# Test with regular value (not a Secret)
comp = CustomComponentWithSecrets(regular_param="regular_string")
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["regular_param"] == "regular_string"
# Test with multiple secrets
env_secret1 = Secret.from_env_var("TEST_API_KEY1")
env_secret2 = Secret.from_env_var("TEST_API_KEY2")
comp = CustomComponentWithSecrets(api_key=env_secret1, token=env_secret2, regular_param="test")
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["api_key"] == env_secret1.to_dict()
assert res["init_parameters"]["api_key"]["type"] == "env_var"
assert res["init_parameters"]["token"] == env_secret2.to_dict()
assert res["init_parameters"]["token"]["type"] == "env_var"
assert res["init_parameters"]["regular_param"] == "test"
def test_component_from_dict_with_secret():
"""Test that serialized Secret dictionaries are automatically deserialized in component_from_dict."""
# Test with EnvVarSecret
env_secret = Secret.from_env_var("TEST_API_KEY")
serialized_secret = env_secret.to_dict()
data = {
"type": generate_qualified_class_name(CustomComponentWithSecrets),
"init_parameters": {"api_key": serialized_secret, "regular_param": "test"},
}
comp = component_from_dict(CustomComponentWithSecrets, data, "test_component")
assert isinstance(comp, CustomComponentWithSecrets)
assert isinstance(comp.api_key, Secret)
assert comp.api_key.type.value == "env_var"
assert comp.regular_param == "test"
# Test with None
data = {
"type": generate_qualified_class_name(CustomComponentWithSecrets),
"init_parameters": {"api_key": None, "regular_param": "test"},
}
comp = component_from_dict(CustomComponentWithSecrets, data, "test_component")
assert comp.api_key is None
assert comp.regular_param == "test"
# Test with regular dict (not a Secret)
data = {
"type": generate_qualified_class_name(CustomComponentWithSecrets),
"init_parameters": {"api_key": {"some": "dict"}, "regular_param": "test"},
}
comp = component_from_dict(CustomComponentWithSecrets, data, "test_component")
assert comp.api_key == {"some": "dict"}
assert comp.regular_param == "test"
# Test with multiple secrets
env_secret1 = Secret.from_env_var("TEST_API_KEY1")
env_secret2 = Secret.from_env_var("TEST_API_KEY2")
data = {
"type": generate_qualified_class_name(CustomComponentWithSecrets),
"init_parameters": {"api_key": env_secret1.to_dict(), "token": env_secret2.to_dict(), "regular_param": "test"},
}
comp = component_from_dict(CustomComponentWithSecrets, data, "test_component")
assert isinstance(comp.api_key, Secret)
assert isinstance(comp.token, Secret)
assert comp.regular_param == "test"
def test_component_from_dict_does_not_mutate_input():
"""default_from_dict must not mutate the caller's data dict when deserializing nested objects."""
serialized_secret = Secret.from_env_var("TEST_API_KEY").to_dict()
data: dict[str, Any] = {
"type": generate_qualified_class_name(CustomComponentWithSecrets),
"init_parameters": {"api_key": serialized_secret, "regular_param": "test"},
}
expected = deepcopy(data)
comp = component_from_dict(CustomComponentWithSecrets, data, "test_component")
# the returned component is correctly deserialized ...
assert isinstance(comp.api_key, Secret)
# ... but the caller's dict is left untouched (the Secret sub-dict is not replaced in place)
assert data == expected
assert isinstance(data["init_parameters"]["api_key"], dict)
def test_component_to_dict_and_from_dict_roundtrip_with_secret():
"""Test that serialization and deserialization work together for Secrets."""
# Test roundtrip with EnvVarSecret
original_secret = Secret.from_env_var("TEST_API_KEY")
comp = CustomComponentWithSecrets(api_key=original_secret)
serialized = component_to_dict(comp, "test_component")
assert serialized["init_parameters"]["api_key"]["type"] == "env_var"
deserialized_comp = component_from_dict(CustomComponentWithSecrets, serialized, "test_component")
assert isinstance(deserialized_comp.api_key, EnvVarSecret)
assert deserialized_comp.api_key.type.value == "env_var"
assert isinstance(original_secret, EnvVarSecret)
assert deserialized_comp.api_key._env_vars == original_secret._env_vars
# Test roundtrip with multiple secrets
env_secret1 = Secret.from_env_var("TEST_API_KEY1")
env_secret2 = Secret.from_env_var("TEST_API_KEY2")
comp = CustomComponentWithSecrets(api_key=env_secret1, token=env_secret2, regular_param="test")
serialized = component_to_dict(comp, "test_component")
assert serialized["init_parameters"]["api_key"]["type"] == "env_var"
assert serialized["init_parameters"]["token"]["type"] == "env_var"
assert serialized["init_parameters"]["regular_param"] == "test"
deserialized_comp = component_from_dict(CustomComponentWithSecrets, serialized, "test_component")
assert isinstance(deserialized_comp.api_key, EnvVarSecret)
assert isinstance(deserialized_comp.token, EnvVarSecret)
assert deserialized_comp.api_key.type.value == "env_var"
assert deserialized_comp.token.type.value == "env_var"
assert deserialized_comp.regular_param == "test"
assert isinstance(env_secret1, EnvVarSecret)
assert isinstance(env_secret2, EnvVarSecret)
assert deserialized_comp.api_key._env_vars == env_secret1._env_vars
assert deserialized_comp.token._env_vars == env_secret2._env_vars
@component
class CustomComponentWithDevice:
def __init__(
self,
device: ComponentDevice | None = None,
other_device: ComponentDevice | None = None,
name: str | None = None,
):
self.device = device
self.other_device = other_device
self.name = name
@component.output_types(value=str)
def run(self, value: str) -> dict[str, str]:
return {"value": value}
def test_component_to_dict_with_component_device():
"""Test that ComponentDevice instances are automatically serialized in component_to_dict."""
# Test with single device (CPU)
device = ComponentDevice.from_single(Device.cpu())
comp = CustomComponentWithDevice(device=device)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["device"] == {"type": "single", "device": "cpu"}
# Test with single device (GPU with id)
device = ComponentDevice.from_single(Device.gpu(1))
comp = CustomComponentWithDevice(device=device)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["device"] == {"type": "single", "device": "cuda:1"}
# Test with None
comp = CustomComponentWithDevice(device=None)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["device"] is None
# Test with multiple devices (device map)
device_map = DeviceMap({"layer1": Device.gpu(0), "layer2": Device.gpu(1)})
device = ComponentDevice.from_multiple(device_map)
comp = CustomComponentWithDevice(device=device)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["device"] == {
"type": "multiple",
"device_map": {"layer1": "cuda:0", "layer2": "cuda:1"},
}
# Test with multiple ComponentDevice params
device1 = ComponentDevice.from_single(Device.cpu())
device2 = ComponentDevice.from_single(Device.gpu(0))
comp = CustomComponentWithDevice(device=device1, other_device=device2, name="test")
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["device"] == {"type": "single", "device": "cpu"}
assert res["init_parameters"]["other_device"] == {"type": "single", "device": "cuda:0"}
assert res["init_parameters"]["name"] == "test"
def test_component_from_dict_with_component_device():
"""Test that serialized ComponentDevice dictionaries are automatically deserialized in component_from_dict."""
# Test with single device (CPU)
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {"device": {"type": "single", "device": "cpu"}, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert isinstance(comp, CustomComponentWithDevice)
assert isinstance(comp.device, ComponentDevice)
assert comp.device.to_torch_str() == "cpu"
assert comp.name == "test"
# Test with single device (GPU with id)
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {"device": {"type": "single", "device": "cuda:1"}, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert isinstance(comp.device, ComponentDevice)
assert comp.device.to_torch_str() == "cuda:1"
# Test with None
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {"device": None, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert comp.device is None
assert comp.name == "test"
# Test with multiple devices (device map)
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {"device": {"type": "multiple", "device_map": {"layer1": "cuda:0", "layer2": "cuda:1"}}},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert isinstance(comp.device, ComponentDevice)
assert comp.device.has_multiple_devices
# Test with regular dict (not a ComponentDevice - different structure)
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {"device": {"some": "dict"}, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert comp.device == {"some": "dict"}
assert comp.name == "test"
# Test with multiple ComponentDevice params
data = {
"type": generate_qualified_class_name(CustomComponentWithDevice),
"init_parameters": {
"device": {"type": "single", "device": "cpu"},
"other_device": {"type": "single", "device": "cuda:0"},
"name": "test",
},
}
comp = component_from_dict(CustomComponentWithDevice, data, "test_component")
assert isinstance(comp.device, ComponentDevice)
assert isinstance(comp.other_device, ComponentDevice)
assert comp.device.to_torch_str() == "cpu"
assert comp.other_device.to_torch_str() == "cuda:0"
assert comp.name == "test"
def test_component_to_dict_and_from_dict_roundtrip_with_component_device():
"""Test that serialization and deserialization work together for ComponentDevice."""
# Test roundtrip with single device
original_device = ComponentDevice.from_single(Device.cpu())
comp = CustomComponentWithDevice(device=original_device)
serialized = component_to_dict(comp, "test_component")
assert serialized["init_parameters"]["device"]["type"] == "single"
deserialized_comp = component_from_dict(CustomComponentWithDevice, serialized, "test_component")
assert isinstance(deserialized_comp.device, ComponentDevice)
assert deserialized_comp.device.to_torch_str() == original_device.to_torch_str()
# Test roundtrip with GPU device
original_device = ComponentDevice.from_single(Device.gpu(2))
comp = CustomComponentWithDevice(device=original_device)
serialized = component_to_dict(comp, "test_component")
deserialized_comp = component_from_dict(CustomComponentWithDevice, serialized, "test_component")
assert deserialized_comp.device.to_torch_str() == "cuda:2"
# Test roundtrip with device map
device_map = DeviceMap({"layer1": Device.gpu(0), "layer2": Device.cpu()})
original_device = ComponentDevice.from_multiple(device_map)
comp = CustomComponentWithDevice(device=original_device)
serialized = component_to_dict(comp, "test_component")
assert serialized["init_parameters"]["device"]["type"] == "multiple"
deserialized_comp = component_from_dict(CustomComponentWithDevice, serialized, "test_component")
assert isinstance(deserialized_comp.device, ComponentDevice)
assert deserialized_comp.device.has_multiple_devices
# Test roundtrip with multiple ComponentDevice params
device1 = ComponentDevice.from_single(Device.cpu())
device2 = ComponentDevice.from_single(Device.gpu(0))
comp = CustomComponentWithDevice(device=device1, other_device=device2, name="test")
serialized = component_to_dict(comp, "test_component")
assert serialized["init_parameters"]["device"]["type"] == "single"
assert serialized["init_parameters"]["other_device"]["type"] == "single"
assert serialized["init_parameters"]["name"] == "test"
deserialized_comp = component_from_dict(CustomComponentWithDevice, serialized, "test_component")
assert isinstance(deserialized_comp.device, ComponentDevice)
assert isinstance(deserialized_comp.other_device, ComponentDevice)
assert deserialized_comp.device.to_torch_str() == "cpu"
assert deserialized_comp.other_device.to_torch_str() == "cuda:0"
assert deserialized_comp.name == "test"
@component
class CustomComponentWithDocumentStore:
def __init__(self, document_store: InMemoryDocumentStore | None = None, name: str | None = None):
self.document_store = document_store
self.name = name
@component.output_types(value=str)
def run(self, value: str) -> dict[str, str]:
return {"value": value}
def test_component_to_dict_with_document_store(in_memory_doc_store):
"""Test that DocumentStore instances are automatically serialized in component_to_dict."""
# Test with InMemoryDocumentStore
comp = CustomComponentWithDocumentStore(document_store=in_memory_doc_store)
res = component_to_dict(comp, "test_component")
assert "type" in res["init_parameters"]["document_store"]
assert "init_parameters" in res["init_parameters"]["document_store"]
assert (
res["init_parameters"]["document_store"]["type"]
== "haystack.document_stores.in_memory.document_store.InMemoryDocumentStore"
)
# Test with None
comp = CustomComponentWithDocumentStore(document_store=None)
res = component_to_dict(comp, "test_component")
assert res["init_parameters"]["document_store"] is None
def test_component_from_dict_with_document_store(in_memory_doc_store):
"""Test that serialized DocumentStore dictionaries are automatically deserialized in component_from_dict."""
# Test with InMemoryDocumentStore
serialized_doc_store = in_memory_doc_store.to_dict()
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {"document_store": serialized_doc_store, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDocumentStore, data, "test_component")
assert isinstance(comp, CustomComponentWithDocumentStore)
assert isinstance(comp.document_store, InMemoryDocumentStore)
assert comp.name == "test"
# Test with None
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {"document_store": None, "name": "test"},
}
comp = component_from_dict(CustomComponentWithDocumentStore, data, "test_component")
assert comp.document_store is None
assert comp.name == "test"
def test_component_to_dict_and_from_dict_roundtrip_with_document_store(in_memory_doc_store):
"""Test that serialization and deserialization work together for DocumentStore."""
# Test roundtrip with InMemoryDocumentStore
comp = CustomComponentWithDocumentStore(document_store=in_memory_doc_store)
serialized = component_to_dict(comp, "test_component")
assert "type" in serialized["init_parameters"]["document_store"]
assert (
serialized["init_parameters"]["document_store"]["type"]
== "haystack.document_stores.in_memory.document_store.InMemoryDocumentStore"
)
deserialized_comp = component_from_dict(CustomComponentWithDocumentStore, serialized, "test_component")
assert isinstance(deserialized_comp.document_store, InMemoryDocumentStore)
assert deserialized_comp.document_store.bm25_algorithm == in_memory_doc_store.bm25_algorithm
assert (
deserialized_comp.document_store.embedding_similarity_function
== in_memory_doc_store.embedding_similarity_function
)
# Test roundtrip with custom parameters
in_memory_doc_store = InMemoryDocumentStore(
bm25_algorithm="BM25Okapi", embedding_similarity_function="cosine", return_embedding=False
)
comp = CustomComponentWithDocumentStore(document_store=in_memory_doc_store)
serialized = component_to_dict(comp, "test_component")
deserialized_comp = component_from_dict(CustomComponentWithDocumentStore, serialized, "test_component")
assert isinstance(deserialized_comp.document_store, InMemoryDocumentStore)
assert deserialized_comp.document_store.bm25_algorithm == "BM25Okapi"
assert deserialized_comp.document_store.embedding_similarity_function == "cosine"
assert deserialized_comp.document_store.return_embedding is False
def test_default_to_dict_with_document_store(in_memory_doc_store):
"""Test that DocumentStore instances are automatically serialized in default_to_dict."""
res = default_to_dict(in_memory_doc_store)
assert res["type"] == "haystack.document_stores.in_memory.document_store.InMemoryDocumentStore"
assert "init_parameters" in res
# Test that DocumentStore is serialized when passed as a parameter
comp = CustomComponentWithDocumentStore(document_store=in_memory_doc_store)
res = default_to_dict(comp, document_store=in_memory_doc_store, name="test")
assert "type" in res["init_parameters"]["document_store"]
assert res["init_parameters"]["name"] == "test"
def test_default_from_dict_with_document_store(in_memory_doc_store):
"""Test that serialized DocumentStore dictionaries are automatically deserialized in default_from_dict."""
serialized = in_memory_doc_store.to_dict()
# Test direct deserialization
deserialized = default_from_dict(InMemoryDocumentStore, serialized)
assert isinstance(deserialized, InMemoryDocumentStore)
assert deserialized.bm25_algorithm == in_memory_doc_store.bm25_algorithm
# Test deserialization when DocumentStore is in init_parameters
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {"document_store": serialized, "name": "test"},
}
comp = default_from_dict(CustomComponentWithDocumentStore, data)
assert isinstance(comp.document_store, InMemoryDocumentStore)
assert comp.name == "test"
def test_default_from_dict_with_invalid_class_name():
"""Test that deserialization raises ImportError with improved message when class cannot be imported."""
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {
# Use a class name that passes the allowlist (haystack.*) but cannot be resolved.
"document_store": {"type": "haystack.does.not.exist.Class", "init_parameters": {}},
"name": "test",
},
}
# Verify the error message includes the parameter key and original error
with pytest.raises(
ImportError, match=r"Failed to deserialize 'document_store':.*haystack\.does\.not\.exist\.Class"
):
default_from_dict(CustomComponentWithDocumentStore, data)
def test_default_from_dict_rejects_untrusted_nested_class():
"""A nested class with a module outside the allowlist should be rejected."""
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {
"document_store": {"type": "nonexistent.module.Class", "init_parameters": {}},
"name": "test",
},
}
with pytest.raises(
DeserializationError, match=r"Failed to deserialize 'document_store':.*not on the trusted-module allowlist"
):
default_from_dict(CustomComponentWithDocumentStore, data)
def test_default_from_dict_rejects_unknown_nested_parameter():
"""A nested ``{type: ...}`` dict on a parameter that the class does not accept must be rejected
before the smuggled type is imported (Option 3: type-aware deserialization)."""
data = {
"type": generate_qualified_class_name(CustomComponentWithDocumentStore),
"init_parameters": {
# `payload` is not an init parameter of CustomComponentWithDocumentStore.
"payload": {"type": "haystack.testing.factory.MyComponent", "init_parameters": {}},
"name": "test",
},
}
with pytest.raises(DeserializationError) as exc_info:
default_from_dict(CustomComponentWithDocumentStore, data)
message = str(exc_info.value)
assert "Refusing to deserialize unknown parameter 'payload'" in message
# The message lists the accepted parameters (sorted) and tells the user how to fix it.
assert "Valid parameters are: 'document_store', 'name'." in message
assert "Correct the parameter name or remove it from the serialized data." in message