Skip to content

Commit feb60bf

Browse files
Copilotstefankoegl
andcommitted
Add expand_slash_keys utility, tests, and fix CI configuration
Co-authored-by: stefankoegl <184196+stefankoegl@users.noreply.github.com> Agent-Logs-Url: https://github.com/stefankoegl/python-json-patch/sessions/d432f792-40d4-47b2-9e6b-8697b7330028
1 parent 91691b8 commit feb60bf

File tree

3 files changed

+86
-2
lines changed

3 files changed

+86
-2
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
fail-fast: false
1111
matrix:
12-
python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"]
12+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
1313

1414
steps:
1515
- uses: actions/checkout@v3
@@ -31,4 +31,4 @@ jobs:
3131
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3232
- name: Test
3333
run: |
34-
coverage run --source=jsonpointer tests.py
34+
coverage run --source=jsonpatch tests.py

jsonpatch.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,58 @@ def _compare_values(self, path, key, src, dst):
924924
self._item_replaced(path, key, dst)
925925

926926

927+
def expand_slash_keys(obj):
928+
"""Expand slash-separated keys in a dict into nested dicts.
929+
930+
Keys containing '/' are split on '/' and expanded into nested
931+
dictionaries. This is useful when a flat dictionary uses slash-separated
932+
path-like keys and you want to generate JSON Patch paths that traverse
933+
nested objects rather than addressing a literal key that contains '/'.
934+
935+
Leading and trailing '/' characters in keys are ignored (empty path
936+
segments are skipped).
937+
938+
Raises :exc:`ValueError` if two keys produce conflicting paths (e.g.
939+
``'a'`` and ``'a/b'`` both appear in *obj*).
940+
941+
:param obj: The flat dictionary whose keys should be expanded.
942+
:type obj: dict
943+
944+
:return: A new dictionary with slash-separated keys expanded into
945+
nested dicts.
946+
:rtype: dict
947+
948+
>>> expand_slash_keys({'/fields/test': '123456'})
949+
{'fields': {'test': '123456'}}
950+
>>> expand_slash_keys({'a/b': 1, 'c': 2}) == {'a': {'b': 1}, 'c': 2}
951+
True
952+
"""
953+
result = {}
954+
for key, value in obj.items():
955+
parts = [p for p in str(key).split('/') if p]
956+
if not parts:
957+
result[key] = value
958+
continue
959+
d = result
960+
for part in parts[:-1]:
961+
if part not in d:
962+
d[part] = {}
963+
elif not isinstance(d[part], dict):
964+
raise ValueError(
965+
"Key conflict: '{0}' is both a value and a path "
966+
"prefix in the source dict".format(part)
967+
)
968+
d = d[part]
969+
last = parts[-1]
970+
if last in d and isinstance(d[last], dict):
971+
raise ValueError(
972+
"Key conflict: '{0}' is both a path prefix and a value "
973+
"in the source dict".format(last)
974+
)
975+
d[last] = value
976+
return result
977+
978+
927979
def _path_join(path, key):
928980
if key is None:
929981
return path

tests.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,38 @@ def test_copy_operation_structure(self):
889889
with self.assertRaises(jsonpatch.JsonPatchConflict):
890890
jsonpatch.CopyOperation({'path': '/target', 'from': '/source'}).apply({})
891891

892+
def test_expand_slash_keys_simple(self):
893+
"""Slashed keys are expanded into nested dicts."""
894+
result = jsonpatch.expand_slash_keys({'/fields/test': '123456'})
895+
self.assertEqual(result, {'fields': {'test': '123456'}})
896+
897+
def test_expand_slash_keys_mixed(self):
898+
"""Keys with and without slashes are handled correctly."""
899+
result = jsonpatch.expand_slash_keys({'a/b': 1, 'c': 2})
900+
self.assertEqual(result, {'a': {'b': 1}, 'c': 2})
901+
902+
def test_expand_slash_keys_deep(self):
903+
"""Keys with multiple slash levels produce deeply nested dicts."""
904+
result = jsonpatch.expand_slash_keys({'a/b/c': 42})
905+
self.assertEqual(result, {'a': {'b': {'c': 42}}})
906+
907+
def test_expand_slash_keys_no_slashes(self):
908+
"""Dicts without slashed keys are returned unchanged."""
909+
result = jsonpatch.expand_slash_keys({'foo': 'bar', 'baz': 1})
910+
self.assertEqual(result, {'foo': 'bar', 'baz': 1})
911+
912+
def test_expand_slash_keys_make_patch(self):
913+
"""expand_slash_keys allows make_patch to produce readable paths."""
914+
src = {}
915+
dst = jsonpatch.expand_slash_keys({'/fields/test': '123456'})
916+
patch = jsonpatch.make_patch(src, dst)
917+
self.assertEqual(patch.patch, [{'op': 'add', 'path': '/fields', 'value': {'test': '123456'}}])
918+
919+
def test_expand_slash_keys_conflict(self):
920+
"""Conflicting keys raise ValueError."""
921+
with self.assertRaises(ValueError):
922+
jsonpatch.expand_slash_keys({'a': 1, 'a/b': 2})
923+
892924

893925
class CustomJsonPointer(jsonpointer.JsonPointer):
894926
pass

0 commit comments

Comments
 (0)