Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/pydantify_common/helper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from lxml import etree
from .model import XMLPydantifyModel
from typing import Any

NETCONF_BASE_NS = "urn:ietf:params:xml:ns:netconf:base:1.0"


def model_dump_xml_string(
Expand All @@ -9,6 +10,8 @@ def model_dump_xml_string(
data = model.model_dump_xml()
if data_root:
# Add `<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">` root element
pass
root = etree.Element("data", nsmap={None: NETCONF_BASE_NS})
root.append(data)
data = root

return etree.tostring(data, encoding=str, pretty_print=pretty_print)
91 changes: 89 additions & 2 deletions src/pydantify_common/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,92 @@ class XMLPydantifyModel(PydantifyModel):
namespace: ClassVar[str]
prefix: ClassVar[str]

def model_dump_xml(self) -> etree.Element:
pass
def fields_to_elements(
self,
container_name: str | None = None,
parent_namespace: str | None = None,
) -> list[etree._Element]:
"""Convert model fields to XML elements.

Args:
container_name: If provided, wrap elements in a container with this name
parent_namespace: The namespace of the parent element, used to determine
if xmlns declaration is needed
"""
elements: list[etree._Element] = []

# Get field info from model class (not instance to avoid deprecation warning)
for field_name, field_info in self.__class__.model_fields.items():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing model_fields is deprecated and also does not support excluding defaults or None. And not really a reason to access it over __class__

value = getattr(self, field_name)
if value is None:
continue

# Get the XML element name from alias or field name
xml_name = field_info.alias if field_info.alias else field_name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why the AI thinks it is a good idea to extract the namespace from the field alias rather than using the classvar. There is a reason the class vars exist, and the alias could be wrong. I don't see it as clean software engineering to magically extract information from a field that is not designed for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay I see why it does it. In case the var name would be different to the field name. this could happen if the field name has a dash in it's name 🤔

# Strip prefix (e.g., "configuration:devicename" -> "devicename")
if ":" in xml_name:
xml_name = xml_name.split(":", 1)[1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of this at all


# Handle XMLPydantifyModel instances (nested models)
if isinstance(value, XMLPydantifyModel):
child_elements = value.fields_to_elements(
container_name=xml_name,
parent_namespace=self.namespace,
)
elements.extend(child_elements)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny that it uses now the classvar

# Handle lists
elif isinstance(value, list):
for item in value:
if isinstance(item, XMLPydantifyModel):
child_elements = item.fields_to_elements(
container_name=xml_name,
parent_namespace=self.namespace,
)
elements.extend(child_elements)
else:
# Primitive list item
elem = etree.Element(xml_name)
elem.text = str(item)
elements.append(elem)
else:
# Primitive field
elem = etree.Element(xml_name)
elem.text = str(value)
elements.append(elem)

# Wrap in container if container_name is provided
if container_name:
# Only add xmlns when namespace differs from parent
if parent_namespace is None or self.namespace != parent_namespace:
root = etree.Element(container_name, nsmap={None: self.namespace})
else:
root = etree.Element(container_name)
for elem in elements:
root.append(elem)
return [root]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works but could be done without nesting if's and for loop. The extra loop is not sexy


return elements

def model_dump_xml(self) -> etree._Element:
"""Convert model to XML element tree."""
# Get the first field and use it as the root element
for field_name, field_info in self.__class__.model_fields.items():
value = getattr(self, field_name)
if value is None:
continue

xml_name = field_info.alias if field_info.alias else field_name
if ":" in xml_name:
xml_name = xml_name.split(":", 1)[1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated (ugly) code


if isinstance(value, XMLPydantifyModel):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really needed as the check is done in fields_to_elements

elements = value.fields_to_elements(
container_name=xml_name,
parent_namespace=None,
)
if elements:
return elements[0]

# Fallback: return self as root
elements = self.fields_to_elements(container_name=self.prefix)
return elements[0] if elements else etree.Element(self.prefix)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the idea of the use of self.prefix is, but this does not make sense at all for me

2 changes: 1 addition & 1 deletion tests/examples/with_augment/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Annotated, List, ClassVar

from pydantic import ConfigDict, Field, RootModel
from pydantic import ConfigDict, Field
from pydantify_common.model import XMLPydantifyModel


Expand Down
2 changes: 1 addition & 1 deletion tests/examples/with_import_uses/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Annotated, List, ClassVar

from pydantic import ConfigDict, Field, RootModel
from pydantic import ConfigDict, Field
from pydantify_common.model import XMLPydantifyModel


Expand Down
1 change: 0 additions & 1 deletion tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from pydantify_common.model import XMLPydantifyModel
from pydantify_common.helper import model_dump_xml_string
from lxml import etree

Expand Down