h2non / jsonpath-ng

Finally, a JSONPath implementation for Python that aims to be standard compliant. That's all. Enjoy!
Apache License 2.0
582 stars 85 forks source link

JSONPath.update_or_create() #72

Closed kaapstorm closed 3 years ago

kaapstorm commented 3 years ago

JSONPath.update_or_create() is like JSONPath.update(), but it can also create new values, for the purpose of building out a JSON document. Here is an example based on tests in tests/test_create.py:

>>> data = {}
>>> jsonpath = parse('$.name[0].text')
>>> data = jsonpath.update_or_create(data, 'Sir Michael')
>>> data
{'name': [{'text': 'Sir Michael'}]}
>>> jsonpath = parse('$.birthDate')
>>> data = jsonpath.update_or_create(data, '1943-05-05')
>>> data
{'name': [{'text': 'Sir Michael'}],
 'birthDate': '1943-05-05'}

Creating new items is only implemented for JSONPath subclasses that make sense for this use case. For example, using Slice to create many values is not supported in this change, nor is using Filter to create items to satisfy filter criteria (although you can still use Filter to select existing items).

This is a second take at PR #71 . It preserves the signatures and behaviour of the current find() and update() methods in order to work with subclasses defined outside this codebase.

fyi @h2non @proteusvacuum

kintarowins commented 3 years ago

This is awesome! Thanks for creating this. How about "inserting" something before or after a path? Can this be easily done?

kaapstorm commented 3 years ago

That is an interesting question @kintarowins ... but I'm not certain I know what you mean by "insert". Do you mean, for example, if you have an object like this ...

data = {
    "foo": [
        {"bar": "baz"},
        {"bar": "bizzle"},
    ]
}

... can you "insert" a "qux": 42 to get a result like ...

{
    "foo": [
        {"bar": "baz", "qux": 42},
        {"bar": "bizzle"}
    ]
}

...?

If that is what you mean, the answer is, "yes". You can do that like so:

from jsonpath_ng.ext import parse

jsonpath_str = 'foo[?bar="baz"].qux'
result = parse(jsonpath_str).update_or_create(data, 42)

Or do you mean something else? If so, could you give an example?

kintarowins commented 3 years ago

@kaapstorm that's sort of what I'm looking for but with the ability to position new items at a certain order. For example

data = {
    "foo": {
        "a": "aa",
        "c": "cc",
        "d": {
            "bar": [1, 2, 3, 5]
        }
    }
}

jsonpath_str = 'foo.B'
parse(jsonpath_str).insert(data, 1, 'BB')       # insert at position 1
jsonpath_str = 'foo.d.bar'
parse(jsonpath_str).insert(data, 3, 'FOUR')     # insert at position 3

result:

data = {
    "foo": {
        "a": "aa",
        "B": "BB",
        "c": "cc",
        "d": {
            "bar": [1, 2, 3, 'FOUR', 5]
        }
    }
}
kaapstorm commented 3 years ago

Ah, thanks @kintarowins now I get it.

Not quite as cool as what you're proposing, but I think you could achieve the same result if you inserted the value using Python, and then updated the list's property with its new value. e.g.

data = {
    "foo": {
        "a": "aa",
        "c": "cc",
        "d": {
            "bar": [1, 2, 3, 5]
        }
    }
}

jsonpath = parse('foo.d.bar')
bar = jsonpath.find(data)[0].value
bar.insert(3, 'FOUR')
data = jsonpath.update_or_create(data, bar)

Your first example is trickier, because (new in Python 3) dictionaries keep their ordering, but I don't think there is a dictionary method to insert a key using an index. You could copy the dictionary:

jsonpath = parse('foo')
foo = jsonpath.find(data)[0].value
keys = list(foo.keys())
keys.insert(1, "B")
foo['B'] = 'BB'
new_foo = {k: foo[k] for k in keys}
data = jsonpath.update_or_create(data, new_foo)

Now data will have the value you want.

SirLich commented 3 years ago

Is there any reason this hasn't been merged in? It looks quite solid, and would be really useful.

The only other option is pretty ugly, sadly.

kaapstorm commented 3 years ago

@SirLich I think it's just waiting for review and approval from @h2non