Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand All @@ -31,4 +31,4 @@ jobs:
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test
run: |
coverage run --source=jsonpointer tests.py
coverage run --source=jsonpatch tests.py
52 changes: 52 additions & 0 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,58 @@ def _compare_values(self, path, key, src, dst):
self._item_replaced(path, key, dst)


def expand_slash_keys(obj):
"""Expand slash-separated keys in a dict into nested dicts.

Keys containing '/' are split on '/' and expanded into nested
dictionaries. This is useful when a flat dictionary uses slash-separated
path-like keys and you want to generate JSON Patch paths that traverse
nested objects rather than addressing a literal key that contains '/'.

Leading and trailing '/' characters in keys are ignored (empty path
segments are skipped).

Raises :exc:`ValueError` if two keys produce conflicting paths (e.g.
``'a'`` and ``'a/b'`` both appear in *obj*).

:param obj: The flat dictionary whose keys should be expanded.
:type obj: dict

:return: A new dictionary with slash-separated keys expanded into
nested dicts.
:rtype: dict

>>> expand_slash_keys({'/fields/test': '123456'})
{'fields': {'test': '123456'}}
>>> expand_slash_keys({'a/b': 1, 'c': 2}) == {'a': {'b': 1}, 'c': 2}
True
"""
result = {}
for key, value in obj.items():
parts = [p for p in str(key).split('/') if p]
if not parts:
result[key] = value
continue
d = result
for part in parts[:-1]:
if part not in d:
d[part] = {}
elif not isinstance(d[part], dict):
raise ValueError(
"Key conflict: '{0}' is both a value and a path "
"prefix in the source dict".format(part)
)
d = d[part]
last = parts[-1]
if last in d and isinstance(d[last], dict):
raise ValueError(
"Key conflict: '{0}' is both a path prefix and a value "
"in the source dict".format(last)
)
d[last] = value
return result


def _path_join(path, key):
if key is None:
return path
Expand Down
32 changes: 32 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,38 @@ def test_copy_operation_structure(self):
with self.assertRaises(jsonpatch.JsonPatchConflict):
jsonpatch.CopyOperation({'path': '/target', 'from': '/source'}).apply({})

def test_expand_slash_keys_simple(self):
"""Slashed keys are expanded into nested dicts."""
result = jsonpatch.expand_slash_keys({'/fields/test': '123456'})
self.assertEqual(result, {'fields': {'test': '123456'}})

def test_expand_slash_keys_mixed(self):
"""Keys with and without slashes are handled correctly."""
result = jsonpatch.expand_slash_keys({'a/b': 1, 'c': 2})
self.assertEqual(result, {'a': {'b': 1}, 'c': 2})

def test_expand_slash_keys_deep(self):
"""Keys with multiple slash levels produce deeply nested dicts."""
result = jsonpatch.expand_slash_keys({'a/b/c': 42})
self.assertEqual(result, {'a': {'b': {'c': 42}}})

def test_expand_slash_keys_no_slashes(self):
"""Dicts without slashed keys are returned unchanged."""
result = jsonpatch.expand_slash_keys({'foo': 'bar', 'baz': 1})
self.assertEqual(result, {'foo': 'bar', 'baz': 1})

def test_expand_slash_keys_make_patch(self):
"""expand_slash_keys allows make_patch to produce readable paths."""
src = {}
dst = jsonpatch.expand_slash_keys({'/fields/test': '123456'})
patch = jsonpatch.make_patch(src, dst)
self.assertEqual(patch.patch, [{'op': 'add', 'path': '/fields', 'value': {'test': '123456'}}])

def test_expand_slash_keys_conflict(self):
"""Conflicting keys raise ValueError."""
with self.assertRaises(ValueError):
jsonpatch.expand_slash_keys({'a': 1, 'a/b': 2})


class CustomJsonPointer(jsonpointer.JsonPointer):
pass
Expand Down