bazelbuild / starlark

Starlark Language
Apache License 2.0
2.48k stars 163 forks source link

How to set value deep in object #204

Open kferrone opened 3 years ago

kferrone commented 3 years ago

I am looking for the ability to arbitrarily set a value deep on a dict using some form of path syntax.

for example:

{
  "a": {
    "b": "foo"
  }
}

Taken I have a variable like this for the path to the value a.b I want to easily set the value to bar

At this moment I have no idea how to easily do this in starlark without having to split on dot and loop through the keys to build an object for dict.update

ndmitchell commented 3 years ago

If the string is static, x["a"]["b"] = "bar" should work.

If you want to specify it as a.b without breaking it apart, then it seems like you want a feature that isn't available in Starlark. But that's OK, lots of features aren't - you can write a function:

def set_deep(dict, path, val):
    ...

Which does what you want - and now you can extend Starlark with that feature. Generally Starlark is a subset of Python, so if you think this is useful, I think the interesting question would be how do you do it in Python, and can Starlark do it the same way or does it lack something. And my guess is in Python you'd do something very much like set_deep? Or is there a better way in Python?

kferrone commented 3 years ago

So after reading the entire spec and also finding out recursive functions are also banned, I actually figured it out. Well I already knew what needed to be done, just wanted to hear there was an easier way. Although this is basically Python, Starlark does also bans imports. So I was unable to import all the fun libraries Python core had to offer like deepSet and deepCopy, ie these are not actually in Python itself.

In my case I am using Starlark with Kustomize.

Just in case someone else out there wants to save a few minutes; here is my painfully manual deepGet and json patch,

# uses a json patch style to patch an objects deep value
# p = { path, op, value }
def patch(p, r):
  ref = r
  parts = p["path"].split("/")
  count = len(parts)
  for i in range(count): # recurse into the resource now to set the patch
    k = parts[i]
    # the first part of this if block is to make sure arrays and objects are initialized correctly
    if not k == parts[-1]: # if k is not the last item in the list
      if not k.isdigit():
        if not k in ref: # we create the structure if the key is not already there
          if parts[i+1].isdigit(): # if the next step is an array accessor
            ref[k] = [] # we need to init the array
          else:
            ref[k] = {}
    # this section is for actually setting the value
    else:
      val = p["value"]
      if k.isdigit():
        ref.insert(int(k), val)
      else:
        # add dict means apply the values to the source
        if type(val) == "dict" and p["op"] == "add":
          ref[k].update(val)
        # everything else basically means replace
        else:
          ref[k] = val
      break
    if k.isdigit():
      k = int(k)
    # unlink the dicts so the replicas are not linked to each other
    if type(ref[k]) == "dict":
      ref[k] = unlink(ref[k])
    if type(ref[k]) == "list":
      ref[k] = list(ref[k])
      for ii in range(len(ref[k])):
        if type(ref[k][ii]) == "dict":
          ref[k][ii] = dict(ref[k][ii])
    ref = ref[k]

# Get a value deep within an object using a path
# example: spec/containers/0/name
def deepGet(path, r):
  ref = r
  parts = path.split("/")
  count = len(parts)
  for i in range(count): # for each index in the parts
    k = parts[i]
    # if the part is a number then the next step will access an array
    if k.isdigit(): 
      k = int(k)
    else:
      if not k in ref: # make sure the key actually exists
        fail("The value at",path,"with key",k,"was not found on",r["kind"]+"/"+r["metadata"]["name"])
    if type(ref[k]) == "dict":
      ref = unlink(ref[k])
    elif type(ref[k]) == "list":
      ref = list(ref[k])
    else:
      ref = ref[k]
  return ref

# quick helper to decide how to unlink a value from its source
# this is only a shallow copy, as good as it's going to get
def unlink(r):
  for k, v in r.items():
    if type(v) == "dict":
      r[k] = dict(v)
    elif type(v) == "list":
      r[k] = list(v)
  return r