Skip to content

fix: to_json no longer mutates caller's ops#33

Merged
berendkleinhaneveld merged 1 commit into
masterfrom
fix/to-json-no-mutate
May 26, 2026
Merged

fix: to_json no longer mutates caller's ops#33
berendkleinhaneveld merged 1 commit into
masterfrom
fix/to-json-no-mutate

Conversation

@berendkleinhaneveld
Copy link
Copy Markdown
Collaborator

Summary

patchdiff.to_json(ops) was mutating the caller's ops. to_str_paths did a shallow ops.copy(), so the inner dicts were still shared. Overwriting op[\"path\"] with str(op[\"path\"]) then replaced the Pointer in the caller's dicts with strings, which broke subsequent apply() calls. Fix by copying each dict ({**op, \"path\": str(op[\"path\"])}) instead of just the outer list.

Reproducer on master

uv run python -c "
from patchdiff import diff, apply, to_json
a = {'x': [1, 2, 3]}
b = {'x': [1, 9, 3]}
ops, _ = diff(a, b)
to_json(ops)
print(apply(a, ops))
"

On master this raises:

Traceback (most recent call last):
  File "<string>", line 7, in <module>
    print(apply(a, ops))
  File "patchdiff/apply.py", line 48, in apply
    return iapply(deepcopy(obj), patches)
  File "patchdiff/apply.py", line 14, in iapply
    parent, key, _ = ptr.evaluate(obj)
AttributeError: 'str' object has no attribute 'evaluate'

On this branch it prints {'x': [1, 9, 3]}.

New test

  • tests/test_serialize.py::test_to_json_does_not_mutate_ops — calls to_json(ops), discards the result, then asserts apply(a, ops) == b. Fails on master with the traceback above; passes on this branch.

Existing tests

  • tests/test_serialize.py::test_to_json still passes (sanity check on the JSON output).
  • Full uv run pytest -x -q is green (269 passed).

Test plan

  • uv run pytest tests/test_serialize.py -v
  • uv run pytest -x -q
  • uv run ruff check patchdiff/serialize.py tests/test_serialize.py
  • uv run ruff format --check patchdiff/serialize.py tests/test_serialize.py

🤖 Generated with Claude Code

to_str_paths used a shallow list copy, so overwriting op["path"] with
str(op["path"]) mutated the caller's Pointer-bearing ops dicts and broke
subsequent apply() calls. Switch to a per-dict copy via a list
comprehension so the originals stay intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@berendkleinhaneveld berendkleinhaneveld merged commit c26b6b6 into master May 26, 2026
10 checks passed
@berendkleinhaneveld berendkleinhaneveld deleted the fix/to-json-no-mutate branch May 26, 2026 10:09
berendkleinhaneveld added a commit that referenced this pull request May 26, 2026
This version includes the following:

* ci: Update versions of steps and let setup-uv manage the python version (#40)
* perf: trim common prefix/suffix in diff_lists (#36)
* perf: use str.replace in Pointer escape/unescape (#37)
* fix: produce() snapshots mutable values when recording patches (#34)
* perf: linear reverse-op construction in diff_dicts/diff_sets (#38)
* fix: iapply raises on invalid patch paths (#35)
* fix: to_json mutating caller's ops (#33)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants