stefankoegl / python-json-patch

Applying JSON Patches in Python
https://python-json-patch.readthedocs.org/
BSD 3-Clause "New" or "Revised" License
459 stars 97 forks source link

Generated patches aren't always the same #151

Open djlambert opened 1 year ago

djlambert commented 1 year ago

On Python 3.6+ (where dict is ordered) I'd expected the following to always return the same results:

import jsonpatch

a = {
    'key1': 'value1',
    'key2': 'value2',
    'key3': 'value3',
    'key4': 'value4',
    'key5': {
        'subkey1': 'subvalue1',
        'subkey2': 'subvalue2',
        'subkey3': 'subvalue3',
        'subkey4': 'subvalue4',
    },
}

b = {
    'key1': '1234',
    'key2': 'asdf',
    'key3': 'value3',
    'key4': 'value4',
    'key5': {
        'subkey1': 'subvalue1',
        'subkey2': 'subvalue2',
        'subkey3': 'subvalue3',
        'subkey5': 'subvalue5',
    },
    'key6': {
        'subkey1': 'subvalue1',
    },
}

print(jsonpatch.JsonPatch.from_diff(a, b).patch)

This isn't the case though:

% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key1', 'value': '1234'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key1', 'value': '1234'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key1', 'value': '1234'}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}]
% python3 misc/json_patch.py
[{'op': 'add', 'path': '/key6', 'value': {'subkey1': 'subvalue1'}}, {'op': 'replace', 'path': '/key2', 'value': 'asdf'}, {'op': 'remove', 'path': '/key5/subkey4'}, {'op': 'add', 'path': '/key5/subkey5', 'value': 'subvalue5'}, {'op': 'replace', 'path': '/key1', 'value': '1234'}]
hf-kklein commented 9 months ago

I ran into this because a unittest was flaky. It's a bit annoying.

djlambert commented 9 months ago

I ran into this because a unittest was flaky. It's a bit annoying.

That was the issue I was running into too

hf-kklein commented 9 months ago

I think I wouldn't even have noticed, if the JsonPatch class had an equality comparison __eq__ which accounts for the fact that the order doesn't matter.

JacobHenner commented 6 months ago

This is caused by the use of sets for comparing dict keys in the internals of the library. Sets (unlike dicts in newer versions of Python) do not preserve order. Ordering is determined by the hash values of objects within the set.

By default, Python randomizes hashes for security purposes. As a workaround for test cases you can disable this behavior by setting PYTHONHASHSEED=0, but you should avoid doing this during actual execution to preserve the security benefits.

I've submitted #161 which I believe will address this issue without requiring PYTHONHASHSEED=0.