From 462ca65b8787efce9bac5d47e792076118f5495e Mon Sep 17 00:00:00 2001 From: Mischa Diehm Date: Mon, 12 Jan 2026 23:20:12 +0100 Subject: [PATCH] Fix XML namespace serialization to only add xmlns when needed The fields_to_elements method now accepts a parent_namespace parameter and only adds xmlns declarations when the child's namespace differs from the parent's namespace. This ensures proper namespace inheritance in XML output. Changes: - Add parent_namespace parameter to fields_to_elements() - Only add nsmap when parent_namespace is None or differs from self.namespace - Implement data_root wrapper in model_dump_xml_string() - Use __class__.model_fields to avoid Pydantic deprecation warnings - Remove unused imports (ruff lint fixes) Co-Authored-By: Claude Opus 4.5 --- src/pydantify_common/helper.py | 7 +- src/pydantify_common/model.py | 91 +++++++++++++++++++++++- tests/examples/with_augment/model.py | 2 +- tests/examples/with_import_uses/model.py | 2 +- tests/test_examples.py | 1 - 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/pydantify_common/helper.py b/src/pydantify_common/helper.py index 689bbfc..d920741 100644 --- a/src/pydantify_common/helper.py +++ b/src/pydantify_common/helper.py @@ -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( @@ -9,6 +10,8 @@ def model_dump_xml_string( data = model.model_dump_xml() if data_root: # Add `` 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) diff --git a/src/pydantify_common/model.py b/src/pydantify_common/model.py index 179cfb3..5265eca 100644 --- a/src/pydantify_common/model.py +++ b/src/pydantify_common/model.py @@ -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(): + 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 + # Strip prefix (e.g., "configuration:devicename" -> "devicename") + if ":" in xml_name: + xml_name = xml_name.split(":", 1)[1] + + # 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) + # 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] + + 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] + + if isinstance(value, XMLPydantifyModel): + 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) diff --git a/tests/examples/with_augment/model.py b/tests/examples/with_augment/model.py index 5e30dde..a2f64bd 100644 --- a/tests/examples/with_augment/model.py +++ b/tests/examples/with_augment/model.py @@ -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 diff --git a/tests/examples/with_import_uses/model.py b/tests/examples/with_import_uses/model.py index cd16020..04da6e3 100644 --- a/tests/examples/with_import_uses/model.py +++ b/tests/examples/with_import_uses/model.py @@ -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 diff --git a/tests/test_examples.py b/tests/test_examples.py index d3e9a75..692e9eb 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -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