Skip to content

Reject diffs that change a member whose name is not a string - #273

Open
stefankoegl wants to merge 1 commit into
masterfrom
claude/make-patch-non-string-keys
Open

stefankoegl wants to merge 1 commit into
masterfrom
claude/make-patch-non-string-keys

Conversation

@stefankoegl

Copy link
Copy Markdown
Owner

The diff turned object keys that are not strings into strings for the paths of its operations. A path can only refer to members whose names are strings, though: /1 is the member '1', not the member 1. The resulting patches were silently wrong:

make_patch(src, dst) Patch Applied to src
{1: 'a'}, {1: 'b'} replace /1 fails: no member '1'
{1: 'a'}, {1: 'a', 2: 'b'} add /2 {1: 'a', '2': 'b'}, not dst
{1: 'a'}, {'1': 'a'} empty src unchanged, not dst

Change

Creating a patch now raises a TypeError if one of its operations would have to refer to such a member:

TypeError: can't create an operation for the object member 1: its name is not a string, which the path of an operation can't refer to

This is narrower than rejecting all non-string keys, which I considered first. Existing tests (test_issue_78_items_not_serializable, test_move_only_values_equal_in_json) diff documents with such keys on purpose, and those patches are correct. So these still work:

  • members with non-string names that are equal in both documents
  • such members inside a value that is added, removed or moved as a whole

Only a difference in, or inside of, such a member raises.

Decision for you

This turns a silently wrong result into an error, which can break callers that diff Python dicts with, say, integer keys and apply the patch to the JSON-serialised form of the document, where the keys are strings and the old patch happened to fit. The alternative would be to keep accepting such keys and convert them the way json.dumps does (True → "true", None → "null"). That stays compatible with those callers, but the patch would still not apply to the Python document it was created from, which is what the audit item was about.

How

Keys that are not strings travel through the DiffBuilder as a private str subclass that remembers the key and is not equal to the string it looks like (so 1 and '1' are different members). The error is raised where paths are written, so it covers every kind of operation, including moves and the append extension.

Verification

  • New tests: 14 document pairs that must raise (each with and without move optimisation and test operations), 11 that must keep working and round-trip.
  • python tests.py and property_tests.py pass with coverage at 100%; flake8 and sphinx-build -W are clean (Python 3.13).
  • No measurable slowdown: diffing two 3000-member documents takes 0.121 s against 0.120 s on master.

Generated by Claude Code

The diff turned object keys that are not strings into strings for the
paths of its operations. A path can only refer to members whose names
are strings, though: '/1' is the member '1', not the member 1. So
make_patch({1: 'a'}, {1: 'b'}) returned a patch that failed on its own
source, make_patch({1: 'a'}, {1: 'a', 2: 'b'}) one that added a member
'2' instead of 2, and make_patch({1: 'a'}, {'1': 'a'}) an empty one.

Creating a patch now raises a TypeError if one of its operations would
have to refer to such a member. Documents with such members can still be
compared as long as these members are equal, or are part of a value that
is added, removed or moved as a whole, as those patches are correct.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same — claude/make-patch-non-string-keys into master

This branch has not been deployed

No deployments
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.

3 participants