2

I am trying to update the xml using object notation using lxml objectify.

<xml>
  <fruit>
    <citrus>
        <lemon />
    </citrus>
  </fruit>  
</xml>

I am trying to add another fruit called mango using lxml objectify like

root = lxml.objectify.fromstring(xml_string)
root.fruit.citrus = 'orange'

def update(path, value):
    // code

update('fruit.citrus', 'orange')

I would like to pass a string like 'fruit.citrus' because I cannot pass an object fruit.citrus.

How do I achieve this in Python ie how do I execute the code 'root.fruit.citrus = 'orange' inside the update function. How to convert string to object?

mzjn
  • 48,958
  • 13
  • 128
  • 248
Vinay
  • 470
  • 1
  • 5
  • 18

3 Answers3

1

Try Below solution:

import lxml.objectify, lxml.etree

xml = '<xml>  <fruit>    <citrus>        <lemon />    </citrus>  </fruit> </xml>'

root = lxml.objectify.fromstring(xml)

print("Before:")
print(lxml.etree.tostring(root))


def update(path, value):
    parent = None
    lst = path.split('.')
    while lst:
        ele = lst.pop(0)
        parent = getattr(root, ele) if parent is None else getattr(parent, ele)
    lxml.etree.SubElement(parent, value)


update('fruit.citrus', 'orange')

print("After:")
print(lxml.etree.tostring(root))

Output:

Before:
b'<xml><fruit><citrus><lemon/></citrus></fruit></xml>'
After:
b'<xml><fruit><citrus><lemon/><orange/></citrus></fruit></xml>'
Chetan Ameta
  • 7,696
  • 3
  • 29
  • 44
  • Could you please add a solution on how we can add indexes as well. For example update('fruit.citrus[1]', 'orange')Thanks – Vinay Mar 13 '20 at 00:51
1

If you insist on using objectify, you may not like this, but I think this is a pretty clean solution using lxml etree:

from lxml import etree

doc = etree.fromstring("""<xml>
  <fruit>
    <citrus>
        <lemon />
    </citrus>
  </fruit>  
</xml>""")


def update(root, path, item):
    elems = root.xpath(path)
    for elem in elems:
        elem.append(etree.Element(item))


update(doc, 'fruit/citrus', 'orange')
print(etree.tostring(doc).decode())
Grismar
  • 27,561
  • 4
  • 31
  • 54
  • Thanks for your answer. I agree it is much cleaner, but I have to use dot notation as I already have code written for it and I am modifying the library by replacing the use of eval. – Vinay Mar 12 '20 at 23:30
  • Is there a reason not to add this line to `update()` and still use it? `path = path.replace('.', '/')` – Grismar Mar 12 '20 at 23:55
1

The above answers were partially correct. It did not have the ability to handle indexes. The below code handles all the cases with ObjectPath (https://lxml.de/objectify.html).

import lxml.objectify, lxml.etree

from robot.api.deco import keyword

class ConfigXML(object):
    def get_xml(self, filename):
        self.root = lxml.objectify.fromstring(open(filename).read())

    def add_attribute(self, path, **kwargs):
        path_obj = lxml.objectify.ObjectPath(path)
        for key in kwargs:
            path_obj.find(self.root).set(key, kwargs[key])

    def add_value(self, path, value):
        path_obj = lxml.objectify.ObjectPath(path)
        path_obj.setattr(self.root, value)

    def add_tag(self, path, tag):
        path_obj = lxml.objectify.ObjectPath(path)
        lxml.objectify.SubElement(path_obj.find(self.root), tag)

    def generate_xml(self):
        lxml.objectify.deannotate(self.root, cleanup_namespaces=True, xsi_nil=True)
        return lxml.etree.tostring(self.root).decode('utf-8')
Vinay
  • 470
  • 1
  • 5
  • 18