feat: preferences dot-path utilities (get_path, set_path)
This commit is contained in:
parent
e6cd3a2e96
commit
9ee31a09c1
3 changed files with 142 additions and 0 deletions
3
circuitforge_core/preferences/__init__.py
Normal file
3
circuitforge_core/preferences/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .paths import get_path, set_path
|
||||||
|
|
||||||
|
__all__ = ["get_path", "set_path"]
|
||||||
64
circuitforge_core/preferences/paths.py
Normal file
64
circuitforge_core/preferences/paths.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Dot-path utilities for reading and writing nested preference dicts.
|
||||||
|
|
||||||
|
All operations are immutable: set_path returns a new dict rather than
|
||||||
|
mutating the input.
|
||||||
|
|
||||||
|
Path format: dot-separated keys, e.g. "affiliate.byok_ids.ebay"
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def get_path(data: dict, path: str, default: Any = None) -> Any:
|
||||||
|
"""Return the value at *path* inside *data*, or *default* if missing.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
prefs = {"affiliate": {"opt_out": False, "byok_ids": {"ebay": "my-id"}}}
|
||||||
|
get_path(prefs, "affiliate.byok_ids.ebay") # "my-id"
|
||||||
|
get_path(prefs, "affiliate.missing", default="x") # "x"
|
||||||
|
"""
|
||||||
|
keys = path.split(".")
|
||||||
|
node: Any = data
|
||||||
|
for key in keys:
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
return default
|
||||||
|
node = node.get(key, _SENTINEL)
|
||||||
|
if node is _SENTINEL:
|
||||||
|
return default
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def set_path(data: dict, path: str, value: Any) -> dict:
|
||||||
|
"""Return a new dict with *value* written at *path*.
|
||||||
|
|
||||||
|
Intermediate dicts are created as needed; existing values at other paths
|
||||||
|
are preserved. The original *data* dict is never mutated.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
prefs = {}
|
||||||
|
updated = set_path(prefs, "affiliate.opt_out", True)
|
||||||
|
# {"affiliate": {"opt_out": True}}
|
||||||
|
"""
|
||||||
|
keys = path.split(".")
|
||||||
|
return _set_recursive(data, keys, value)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_SENTINEL = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _set_recursive(node: Any, keys: list[str], value: Any) -> dict:
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
node = {}
|
||||||
|
key, rest = keys[0], keys[1:]
|
||||||
|
if rest:
|
||||||
|
child = _set_recursive(node.get(key, {}), rest, value)
|
||||||
|
else:
|
||||||
|
child = value
|
||||||
|
return {**node, key: child}
|
||||||
75
tests/test_preferences.py
Normal file
75
tests/test_preferences.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
"""Tests for circuitforge_core.preferences path utilities."""
|
||||||
|
import pytest
|
||||||
|
from circuitforge_core.preferences import get_path, set_path
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetPath:
|
||||||
|
def test_top_level_key(self):
|
||||||
|
assert get_path({"a": 1}, "a") == 1
|
||||||
|
|
||||||
|
def test_nested_key(self):
|
||||||
|
data = {"affiliate": {"opt_out": False}}
|
||||||
|
assert get_path(data, "affiliate.opt_out") is False
|
||||||
|
|
||||||
|
def test_deeply_nested(self):
|
||||||
|
data = {"affiliate": {"byok_ids": {"ebay": "my-tag"}}}
|
||||||
|
assert get_path(data, "affiliate.byok_ids.ebay") == "my-tag"
|
||||||
|
|
||||||
|
def test_missing_key_returns_default(self):
|
||||||
|
assert get_path({}, "missing", default="x") == "x"
|
||||||
|
|
||||||
|
def test_missing_nested_returns_default(self):
|
||||||
|
assert get_path({"a": {}}, "a.b.c", default=42) == 42
|
||||||
|
|
||||||
|
def test_default_is_none_when_omitted(self):
|
||||||
|
assert get_path({}, "nope") is None
|
||||||
|
|
||||||
|
def test_non_dict_intermediate_returns_default(self):
|
||||||
|
assert get_path({"a": "string"}, "a.b", default="d") == "d"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetPath:
|
||||||
|
def test_top_level_key(self):
|
||||||
|
result = set_path({}, "opt_out", True)
|
||||||
|
assert result == {"opt_out": True}
|
||||||
|
|
||||||
|
def test_nested_key_created(self):
|
||||||
|
result = set_path({}, "affiliate.opt_out", True)
|
||||||
|
assert result == {"affiliate": {"opt_out": True}}
|
||||||
|
|
||||||
|
def test_deeply_nested(self):
|
||||||
|
result = set_path({}, "affiliate.byok_ids.ebay", "my-tag")
|
||||||
|
assert result == {"affiliate": {"byok_ids": {"ebay": "my-tag"}}}
|
||||||
|
|
||||||
|
def test_preserves_sibling_keys(self):
|
||||||
|
data = {"affiliate": {"opt_out": False, "byok_ids": {}}}
|
||||||
|
result = set_path(data, "affiliate.opt_out", True)
|
||||||
|
assert result["affiliate"]["opt_out"] is True
|
||||||
|
assert result["affiliate"]["byok_ids"] == {}
|
||||||
|
|
||||||
|
def test_preserves_unrelated_top_level_keys(self):
|
||||||
|
data = {"other": "value", "affiliate": {"opt_out": False}}
|
||||||
|
result = set_path(data, "affiliate.opt_out", True)
|
||||||
|
assert result["other"] == "value"
|
||||||
|
|
||||||
|
def test_does_not_mutate_original(self):
|
||||||
|
data = {"affiliate": {"opt_out": False}}
|
||||||
|
set_path(data, "affiliate.opt_out", True)
|
||||||
|
assert data["affiliate"]["opt_out"] is False
|
||||||
|
|
||||||
|
def test_overwrites_existing_value(self):
|
||||||
|
data = {"affiliate": {"byok_ids": {"ebay": "old-tag"}}}
|
||||||
|
result = set_path(data, "affiliate.byok_ids.ebay", "new-tag")
|
||||||
|
assert result["affiliate"]["byok_ids"]["ebay"] == "new-tag"
|
||||||
|
|
||||||
|
def test_non_dict_intermediate_replaced(self):
|
||||||
|
data = {"affiliate": "not-a-dict"}
|
||||||
|
result = set_path(data, "affiliate.opt_out", True)
|
||||||
|
assert result == {"affiliate": {"opt_out": True}}
|
||||||
|
|
||||||
|
def test_roundtrip_get_after_set(self):
|
||||||
|
prefs = {}
|
||||||
|
prefs = set_path(prefs, "affiliate.opt_out", True)
|
||||||
|
prefs = set_path(prefs, "affiliate.byok_ids.ebay", "tag-123")
|
||||||
|
assert get_path(prefs, "affiliate.opt_out") is True
|
||||||
|
assert get_path(prefs, "affiliate.byok_ids.ebay") == "tag-123"
|
||||||
Loading…
Reference in a new issue