Search code examples
pythonruamel.yaml

ruamel.yaml: insert a value given dynamic index and unknown depth


Using ruamel, I need to add new values to a yaml file where the index (and depth) is not known until runtime. Is there a way to insert a value if I'm given the entire path for the index? (I can decide the format for the index)

i.e. Given the following yaml:

app:
  datasource:
    url:example.org
    username:myuser
    password:mypw

I need to add a new value to this yaml file. If I'm given the index app:datasource:port and value myport, then I want the output to be:

app:
  datasource:
    url:example.org
    username:myuser
    password:mypw
    port:myport

What I tried

I tried just putting the entire index inside square brackets [] or use .insert(), but neither of these work:

import sys
from ruamel.yaml import YAML

yaml = YAML()
with open('myfile.yaml', 'r') as stream:
  code = yaml.load(stream)

  # assume this index isn't known until runtime
  index = 'app:datasource:port'
  other_index = 'app:datasource:other_port'

  code[index] = 'myport'
  code.insert(1, other_index, 'otherport')

  yaml.dump(code, sys.stdout)

it produces the following output which isn't what I want. It just treats them as a top level index rather than a nested one:

app:
  datasource:
    url:example.org
    username:myuser
    password:mypw
app:datasource:port: myport
app:datasource:other_port: otherport

What can I do to insert a value at a dynamically provided index?


Solution

  • I think your input file doesn't load to what you expect it loads to. The value for datasource is not a mapping, but unquoted multi-line string. That is because there has to be a space after the value indicator (:), unless both the key and value are quoted. Since your index and other_index are just strings (that happen to have a colon in them), these are used as keys for assigning to the root level mapping (your code).

    When you load a YAML document, such as yours, that has a mapping at the root, you'll get an instance of ruamel.yaml.comments.CommentedMap as a result. That instance has a method, mlget, that takes a list of "index-segments", that are used to recursively traverse the data structure. You can use that to easily find the parent of your "index" and set the value.

    Assuming you put a space after the colon of the last three lines of your input:

    import sys
    from pathlib import Path
    import ruamel.yaml
    
    
    def _mlput(self, path, value, index=None, sep=':'):
        spath = path.split(sep)
        parent = self.mlget(spath[:-1])
        if index is None:
            parent[spath[-1]] = value
        else:
            parent.insert(index, spath[-1], value)
    
    ruamel.yaml.comments.CommentedMap.mlput = _mlput
        
    path = Path('myfile.yaml')
    
    yaml = ruamel.yaml.YAML()
    data = yaml.load(path)
    data.mlput('app:datasource:port', 'myport')
    data.mlput('app:datasource:other_port', 'otherport', 1)
    
    yaml.dump(data, sys.stdout)
    

    which gives:

    app:
      datasource:
        url: example.org
        other_port: otherport
        username: myuser
        password: mypw
        port: myport