pydantic-xml icon indicating copy to clipboard operation
pydantic-xml copied to clipboard

How to use XPATH / conditions in wrapped()

Open mehmax opened this issue 10 months ago • 1 comments

Firstly thanks for this amazing package!

I am trying to use an XPATH expression in my wrapped field definition, but it seems like it is not applied correctly.

Below is an example where I want to map the product_attribute/value element to the name attribute in my Class only if the product_attribute/type is name

When using lxml this can be achieved via the XPATH expression product_attribute/[type='name']/value. When I try the same expression in the wrapped function, it does not work.

Now my questions:

  • Are XPATHs even supported?
  • How could I apply a condition in an other way?

from pydantic_xml import BaseXmlModel, wrapped
from lxml import etree


if __name__ == "__main__":

    xml_str = """
    <product>
        <product_attribute>
            <type>name</type>
            <value>Product Name</value>
        </product_attribute>
        <product_attribute>
            <type>price</type>
            <value>1</value>
        </product_attribute>
    </product>

    """

    # using lxml & XPATH
    tree = etree.fromstring(xml_str)
    print("Product Name: ", tree.findtext("product_attribute/[type='name']/value"))


    # using pydantic-yml without XPATH
    class Product(BaseXmlModel, search_mode="ordered", tag="product"):
        name: str = wrapped("product_attribute/value")

    print("Product Name without condition: ", Product.from_xml(xml_str).name)

    # using pydantic-yml with condition
    class Product(BaseXmlModel, search_mode="ordered", tag="product"):
        name: str = wrapped("product_attribute/[type='name']/value")

    print("Product Name with condition: ", Product.from_xml(xml_str).name)


Many thanks in advance!

mehmax avatar Apr 04 '25 11:04 mehmax

@mehmax Hi,

Thanks for your feedback. The library doesn't support xpath expressions as well as wrapped.

You could declare attributes as a list and define a property:

class ProductAttribute(BaseXmlModel):
    type: str = element()
    value: str = element()


class Product(BaseXmlModel, search_mode="ordered", tag="product"):
    attributes: list[ProductAttribute] = element("product_attribute")

    @property
    def name(self) -> str:
        return next(attr for attr in self.attributes if attr.type == 'name').value

product = Product.from_xml(xml_str)
print("Product Name: ", product.name)

or define a map if the attributes are not supposed to be modified:

class ProductAttribute(BaseXmlModel):
    model_config = pydantic.ConfigDict(frozen=True)

    type: str = element()
    value: str = element()


class Product(BaseXmlModel, search_mode="ordered", tag="product"):
    attributes_list: tuple[ProductAttribute, ...] = element("product_attribute")

    @functools.cached_property
    def attributes(self) -> dict[str, str]:
        return {attr.type: attr.value for attr in self.attributes_list}

product = Product.from_xml(xml_str)
print("Product Name: ", product.attributes['name'])

dapper91 avatar Apr 05 '25 07:04 dapper91