Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions qualibrate_config/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,23 @@ def recursive_update_dict(
to_update: RawConfigType,
updates: Mapping[str, Any],
) -> RawConfigType:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain what this function is doing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added docstring, does this clarify it?

"""
Recursively merge nested dictionaries, updating values and adding new keys.

Used to merge project-specific config overrides on top of base config.
New keys are added, existing nested dicts are merged recursively, and
non-dict values are replaced. Modifies ``to_update`` in-place.

Args:
to_update: Base dictionary to update (modified in-place).
updates: Dictionary with updates to apply recursively.

Returns:
The updated dictionary (same reference as ``to_update``).
"""
for k, v in updates.items():
if k not in to_update:
to_update[k] = v
continue
if isinstance(v, Mapping):
to_update[k] = recursive_update_dict(to_update.get(k, {}), v)
Expand Down
131 changes: 131 additions & 0 deletions tests/unit/test_core/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from qualibrate_config.core.utils import recursive_update_dict


def test_recursive_update_dict_add_new_key():
"""Test that new keys from updates are added to the base dict."""
base = {"section": {"existing": "value"}}
updates = {"section": {"new_key": "new_value"}}

result = recursive_update_dict(base, updates)

assert result["section"]["existing"] == "value"
assert result["section"]["new_key"] == "new_value"


def test_recursive_update_dict_update_existing_key():
"""Test that existing keys are properly updated."""
base = {"section": {"key": "old_value"}}
updates = {"section": {"key": "new_value"}}

result = recursive_update_dict(base, updates)

assert result["section"]["key"] == "new_value"


def test_recursive_update_dict_add_new_top_level_section():
"""Test that completely new sections are added."""
base = {"existing_section": {"key": "value"}}
updates = {"new_section": {"key": "value"}}

result = recursive_update_dict(base, updates)

assert "existing_section" in result
assert "new_section" in result
assert result["new_section"]["key"] == "value"


def test_recursive_update_dict_nested_merge_with_new_keys():
"""Test recursive merge preserves existing values while adding new ones."""
base = {"quam": {"version": 3, "serialization": {"include_defaults": True}}}
updates = {
"quam": {
"state_path": "/new/path",
"serialization": {"other_option": False},
}
}

result = recursive_update_dict(base, updates)

# Existing values preserved
assert result["quam"]["version"] == 3
assert result["quam"]["serialization"]["include_defaults"] is True

# New values added
assert result["quam"]["state_path"] == "/new/path"
assert result["quam"]["serialization"]["other_option"] is False


def test_recursive_update_dict_project_config_use_case():
"""
Comment thread
nulinspiratie marked this conversation as resolved.
Test adding new keys to project config when base config doesn't have them.

This test verifies that when merging configurations:
- Base config: [quam] has version=3, no state_path
- Project config: [quam] adds state_path="/project/path"
- Expected: state_path should be merged in while preserving existing values
"""
# Base config from ~/.qualibrate/config.toml
base = {
"quam": {
"raise_error_missing_reference": False,
"version": 3,
"serialization": {"include_defaults": True},
},
"qualibrate": {"version": 5, "project": "CS_1"},
}

# Project config from ~/.qualibrate/projects/CS_1/config.toml
updates = {
"quam": {"state_path": "/path/to/project/quam_state"},
"qualibrate": {"storage": {"location": "/project/storage"}},
}

result = recursive_update_dict(base, updates)

# Verify all base values preserved
assert result["quam"]["version"] == 3
assert result["quam"]["raise_error_missing_reference"] is False
assert result["quam"]["serialization"]["include_defaults"] is True
assert result["qualibrate"]["version"] == 5
assert result["qualibrate"]["project"] == "CS_1"

# Verify new project-specific values added
assert result["quam"]["state_path"] == "/path/to/project/quam_state"
assert result["qualibrate"]["storage"]["location"] == "/project/storage"


def test_recursive_update_dict_empty_updates():
"""Test that empty updates dict doesn't break anything."""
base = {"section": {"key": "value"}}
updates = {}

result = recursive_update_dict(base, updates)

assert result == base


def test_recursive_update_dict_non_dict_value_override():
"""Test that non-dict values properly override nested dicts."""
base = {"section": {"key": {"nested": "value"}}}
updates = {"section": {"key": "simple_value"}}

result = recursive_update_dict(base, updates)

assert result["section"]["key"] == "simple_value"


def test_recursive_update_dict_multiple_new_keys_at_different_levels():
"""Test adding new keys at multiple nesting levels."""
base = {"level1": {"level2": {"existing": "value"}}}
updates = {
"level1": {
"new_at_level2": "value2",
"level2": {"new_at_level3": "value3"},
}
}

result = recursive_update_dict(base, updates)

assert result["level1"]["level2"]["existing"] == "value"
assert result["level1"]["new_at_level2"] == "value2"
assert result["level1"]["level2"]["new_at_level3"] == "value3"