python-json-patch icon indicating copy to clipboard operation
python-json-patch copied to clipboard

Generated patches aren't always the same

Open djlambert opened this issue 2 years ago • 4 comments

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'}]

djlambert avatar Jul 26 '23 20:07 djlambert

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

hf-kklein avatar Feb 15 '24 17:02 hf-kklein

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

That was the issue I was running into too

djlambert avatar Feb 15 '24 17:02 djlambert

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.

hf-kklein avatar Feb 15 '24 18:02 hf-kklein

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.

JacobHenner avatar May 15 '24 01:05 JacobHenner