Skip to content

Commit 6162157

Browse files
feat: add Document.merge() method
1 parent 4731b1d commit 6162157

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/yamltrip/document.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,50 @@ def sync(self, *keys: KeyPart, value: Any) -> Document:
543543
op = _core.Op.replace(value)
544544
return self._apply_patches([_core.Patch(route=route, operation=op)])
545545

546+
def merge(self, *keys: KeyPart, value: Any) -> Document:
547+
"""Merge a value into the mapping at path without removing extra keys.
548+
549+
Recursively updates matching keys and adds new keys, but never
550+
removes existing keys not present in *value*. Lists are replaced
551+
entirely (not merged element-wise).
552+
"""
553+
from yamltrip.sync import _compute_patches # noqa: PLC0415
554+
555+
normalized = _normalize_keys(keys) if keys else ()
556+
557+
# If path doesn't exist, delegate to upsert.
558+
if normalized:
559+
route = _make_route(normalized)
560+
if not self._core_doc.query_exists(route):
561+
return self.upsert(*normalized, value=value)
562+
563+
# Get current value and diff
564+
try:
565+
old_value = self._core_doc.parse_value(_make_route(normalized))
566+
except (ValueError, KeyError):
567+
return self.upsert(*normalized, value=value)
568+
569+
# Pre-convert any flow sequences that will be modified.
570+
doc: Document = self
571+
flow_patches = _flow_seq_replacements(
572+
self._core_doc, old_value, value, normalized
573+
)
574+
if flow_patches:
575+
doc = doc._apply_patches(flow_patches)
576+
old_value = doc._core_doc.parse_value(_make_route(normalized))
577+
578+
patches = _compute_patches(old_value, value, normalized, remove_extra=False)
579+
if not patches:
580+
return doc
581+
try:
582+
return doc._apply_patches(patches)
583+
except PatchError as e:
584+
if "expected BlockSequence" not in str(e):
585+
raise
586+
route = _make_route(normalized)
587+
op = _core.Op.replace(value)
588+
return self._apply_patches([_core.Patch(route=route, operation=op)])
589+
546590
def find_index(self, *keys: KeyPart, where: dict[str, Any]) -> int | None:
547591
"""Return the index of the first list item matching all key/value pairs.
548592

tests/test_merge.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from yamltrip.document import Document
2+
3+
4+
class TestMergeMapping:
5+
def test_keeps_extra_keys(self):
6+
doc = Document("a: 1\nb: 2\nc: 3\n")
7+
doc2 = doc.merge(value={"a": 10, "d": 4})
8+
assert doc2["a"] == 10
9+
assert doc2["b"] == 2
10+
assert doc2["c"] == 3
11+
assert doc2["d"] == 4
12+
13+
def test_nested_merge(self):
14+
doc = Document("db:\n host: localhost\n port: 5432\n user: admin\n")
15+
doc2 = doc.merge("db", value={"port": 3306, "ssl": True})
16+
assert doc2["db", "host"] == "localhost"
17+
assert doc2["db", "port"] == 3306
18+
assert doc2["db", "user"] == "admin"
19+
assert doc2["db", "ssl"] is True
20+
21+
def test_deeply_nested_merge(self):
22+
doc = Document("a:\n b:\n c: 1\n d: 2\n e: 3\n")
23+
doc2 = doc.merge("a", value={"b": {"c": 99}})
24+
assert doc2["a", "b", "c"] == 99
25+
assert doc2["a", "b", "d"] == 2
26+
assert doc2["a", "e"] == 3

0 commit comments

Comments
 (0)