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: 7 additions & 0 deletions .chronus/changes/python-xml-test-cases-2026-3-19-22-47-48.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-client-python"
---

Add mock API test cases for XML scenarios introduced in https://github.com/microsoft/typespec/pull/10063, covering: renamed property, nested model, renamed nested model, wrapped primitive with custom item names, model array variants (wrapped/unwrapped/renamed), renamed attribute, namespace, and namespace-on-properties. Tests for unwrapped model array serialization and namespace handling are skipped pending generator bug fixes.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ celerybeat-schedule
/.env
.env.local
.venv
.venv_test
env/
venv/
ENV/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,11 @@ class Model(_MyMutableMapping):
for rf in self._attr_to_rest_field.values():
prop_meta = getattr(rf, "_xml", {})
xml_name = prop_meta.get("name", rf._rest_name)
xml_ns = prop_meta.get("ns", model_meta.get("ns", None))
xml_ns = prop_meta.get("ns") or prop_meta.get("namespace")
# If the property has no namespace but the model uses a default namespace
# (declared without a prefix), child elements inherit that namespace.
if xml_ns is None and not model_meta.get("prefix"):
xml_ns = model_meta.get("ns") or model_meta.get("namespace")
if xml_ns:
xml_name = "{" + xml_ns + "}" + xml_name

Expand Down Expand Up @@ -761,7 +765,9 @@ class Model(_MyMutableMapping):
model_meta = getattr(cls, "_xml", {})
prop_meta = getattr(discriminator, "_xml", {})
xml_name = prop_meta.get("name", discriminator._rest_name)
xml_ns = prop_meta.get("ns", model_meta.get("ns", None))
xml_ns = prop_meta.get("ns") or prop_meta.get("namespace")
if xml_ns is None and not model_meta.get("prefix"):
xml_ns = model_meta.get("ns") or model_meta.get("namespace")
if xml_ns:
xml_name = "{" + xml_ns + "}" + xml_name

Expand Down Expand Up @@ -1254,10 +1260,15 @@ def _get_element(

# if prop is a model, then use the prop element directly, else generate a wrapper of model
if wrapped_element is None:
# When serializing as an array item (parent_meta is set), check if the parent has an
# explicit itemsName. This ensures correct element names for unwrapped arrays (where
# the element tag is the property/items name, not the model type name).
_items_name = parent_meta.get("itemsName") if parent_meta is not None else None
element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__)
wrapped_element = _create_xml_element(
model_meta.get("name", o.__class__.__name__),
element_name,
model_meta.get("prefix"),
model_meta.get("ns"),
model_meta.get("ns") or model_meta.get("namespace"),
)

readonly_props = []
Expand All @@ -1279,7 +1290,9 @@ def _get_element(
# additional properties will not have rest field, use the wire name as xml name
prop_meta = {"name": k}

# if no ns for prop, use model's
# Propagate model namespace to properties only for old-style "ns"-keyed models.
# DPG-generated models use the "namespace" key and explicitly declare namespace on
# each property that needs it, so propagation is intentionally skipped for them.
if prop_meta.get("ns") is None and model_meta.get("ns"):
prop_meta["ns"] = model_meta.get("ns")
prop_meta["prefix"] = model_meta.get("prefix")
Expand All @@ -1292,9 +1305,10 @@ def _get_element(
wrapped_element.text = _get_primitive_type_value(v)
elif prop_meta.get("attribute", False):
xml_name = prop_meta.get("name", k)
if prop_meta.get("ns"):
ET.register_namespace(prop_meta.get("prefix"), prop_meta.get("ns")) # pyright: ignore
xml_name = "{" + prop_meta.get("ns") + "}" + xml_name # pyright: ignore
_attr_ns = prop_meta.get("ns") or prop_meta.get("namespace")
if _attr_ns:
ET.register_namespace(prop_meta.get("prefix"), _attr_ns) # pyright: ignore
xml_name = "{" + _attr_ns + "}" + xml_name # pyright: ignore
# attribute should be primitive type
wrapped_element.set(xml_name, _get_primitive_type_value(v))
else:
Expand All @@ -1312,7 +1326,7 @@ def _get_element(
exclude_readonly,
{
"name": k,
"ns": parent_meta.get("ns") if parent_meta else None,
"ns": (parent_meta.get("ns") or parent_meta.get("namespace")) if parent_meta else None,
"prefix": parent_meta.get("prefix") if parent_meta else None,
},
)
Expand All @@ -1327,7 +1341,7 @@ def _get_element(
{
"name": parent_meta.get("itemsName", parent_meta.get("name")),
"prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")),
"ns": parent_meta.get("itemsNs", parent_meta.get("ns")),
"ns": parent_meta.get("itemsNs") or parent_meta.get("ns") or parent_meta.get("namespace"),
},
)

Expand All @@ -1340,7 +1354,7 @@ def _get_wrapped_element(
meta: typing.Optional[dict[str, typing.Any]],
) -> ET.Element:
wrapped_element = _create_xml_element(
meta.get("name") if meta else None, meta.get("prefix") if meta else None, meta.get("ns") if meta else None
meta.get("name") if meta else None, meta.get("prefix") if meta else None, (meta.get("ns") or meta.get("namespace")) if meta else None
)
if isinstance(v, (dict, list)):
wrapped_element.extend(_get_element(v, exclude_readonly, meta))
Expand All @@ -1365,7 +1379,16 @@ def _create_xml_element(
tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None
) -> ET.Element:
if prefix and ns:
ET.register_namespace(prefix, ns)
try:
ET.register_namespace(prefix, ns)
except ValueError:
# Some prefixes (e.g. 'ns2') match Python's reserved 'ns\d+' pattern used for
# auto-generated prefixes, causing register_namespace to raise ValueError.
# Fall back to directly registering in the internal namespace map so the intended
# prefix is honoured when serialising to XML.
_ns_map = getattr(ET, "_namespace_map", None)
if _ns_map is not None:
_ns_map[ns] = prefix
if ns:
return ET.Element("{" + ns + "}" + tag)
return ET.Element(tag)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
import pytest
from payload.xml.aio import XmlClient
from payload.xml.models import (
Author,
Book,
SimpleModel,
ModelWithSimpleArrays,
ModelWithArrayOfModel,
ModelWithAttributes,
ModelWithUnwrappedArray,
ModelWithUnwrappedModelArray,
ModelWithRenamedArrays,
ModelWithRenamedProperty,
ModelWithRenamedAttribute,
ModelWithRenamedNestedModel,
ModelWithRenamedWrappedModelArray,
ModelWithRenamedUnwrappedModelArray,
ModelWithRenamedWrappedAndItemModelArray,
ModelWithOptionalField,
ModelWithRenamedFields,
ModelWithEmptyArray,
Expand All @@ -21,6 +30,10 @@
ModelWithEncodedNames,
ModelWithEnum,
ModelWithDatetime,
ModelWithNamespace,
ModelWithNamespaceOnProperties,
ModelWithNestedModel,
ModelWithWrappedPrimitiveCustomItemNames,
)


Expand All @@ -37,13 +50,27 @@ async def test_simple_model(client: XmlClient):
await client.simple_model_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_property(client: XmlClient):
model = ModelWithRenamedProperty(title="foo", author="bar")
assert await client.model_with_renamed_property_value.get() == model
await client.model_with_renamed_property_value.put(model)


@pytest.mark.asyncio
async def test_model_with_simple_arrays(client: XmlClient):
model = ModelWithSimpleArrays(colors=["red", "green", "blue"], counts=[1, 2])
assert await client.model_with_simple_arrays_value.get() == model
await client.model_with_simple_arrays_value.put(model)


@pytest.mark.asyncio
async def test_model_with_wrapped_primitive_custom_item_names(client: XmlClient):
model = ModelWithWrappedPrimitiveCustomItemNames(tags=["fiction", "classic"])
assert await client.model_with_wrapped_primitive_custom_item_names_value.get() == model
await client.model_with_wrapped_primitive_custom_item_names_value.put(model)


@pytest.mark.asyncio
async def test_model_with_array_of_model(client: XmlClient):
model = ModelWithArrayOfModel(
Expand All @@ -56,13 +83,68 @@ async def test_model_with_array_of_model(client: XmlClient):
await client.model_with_array_of_model_value.put(model)


@pytest.mark.asyncio
async def test_model_with_unwrapped_model_array(client: XmlClient):
model = ModelWithUnwrappedModelArray(
items_property=[
SimpleModel(name="foo", age=123),
SimpleModel(name="bar", age=456),
]
)
assert await client.model_with_unwrapped_model_array_value.get() == model
await client.model_with_unwrapped_model_array_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_wrapped_model_array(client: XmlClient):
model = ModelWithRenamedWrappedModelArray(
items_property=[
SimpleModel(name="foo", age=123),
SimpleModel(name="bar", age=456),
]
)
assert await client.model_with_renamed_wrapped_model_array_value.get() == model
await client.model_with_renamed_wrapped_model_array_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_unwrapped_model_array(client: XmlClient):
model = ModelWithRenamedUnwrappedModelArray(
items_property=[
SimpleModel(name="foo", age=123),
SimpleModel(name="bar", age=456),
]
)
assert await client.model_with_renamed_unwrapped_model_array_value.get() == model
await client.model_with_renamed_unwrapped_model_array_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_wrapped_and_item_model_array(client: XmlClient):
model = ModelWithRenamedWrappedAndItemModelArray(
books=[
Book(title="The Great Gatsby"),
Book(title="Les Miserables"),
]
)
assert await client.model_with_renamed_wrapped_and_item_model_array_value.get() == model
await client.model_with_renamed_wrapped_and_item_model_array_value.put(model)


@pytest.mark.asyncio
async def test_model_with_attributes(client: XmlClient):
model = ModelWithAttributes(id1=123, id2="foo", enabled=True)
assert await client.model_with_attributes_value.get() == model
await client.model_with_attributes_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_attribute(client: XmlClient):
model = ModelWithRenamedAttribute(id=123, title="The Great Gatsby", author="F. Scott Fitzgerald")
assert await client.model_with_renamed_attribute_value.get() == model
await client.model_with_renamed_attribute_value.put(model)


@pytest.mark.asyncio
async def test_model_with_unwrapped_array(client: XmlClient):
model = ModelWithUnwrappedArray(colors=["red", "green", "blue"], counts=[1, 2])
Expand All @@ -84,6 +166,20 @@ async def test_model_with_optional_field(client: XmlClient):
await client.model_with_optional_field_value.put(model)


@pytest.mark.asyncio
async def test_model_with_nested_model(client: XmlClient):
model = ModelWithNestedModel(nested=SimpleModel(name="foo", age=123))
assert await client.model_with_nested_model_value.get() == model
await client.model_with_nested_model_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_nested_model(client: XmlClient):
model = ModelWithRenamedNestedModel(author=Author(name="foo"))
assert await client.model_with_renamed_nested_model_value.get() == model
await client.model_with_renamed_nested_model_value.put(model)


@pytest.mark.asyncio
async def test_model_with_renamed_fields(client: XmlClient):
model = ModelWithRenamedFields(
Expand Down Expand Up @@ -141,6 +237,20 @@ async def test_model_with_datetime(client: XmlClient):
await client.model_with_datetime_value.put(model)


@pytest.mark.asyncio
async def test_model_with_namespace(client: XmlClient):
model = ModelWithNamespace(id=123, title="The Great Gatsby")
assert await client.model_with_namespace_value.get() == model
await client.model_with_namespace_value.put(model)


@pytest.mark.asyncio
async def test_model_with_namespace_on_properties(client: XmlClient):
model = ModelWithNamespaceOnProperties(id=123, title="The Great Gatsby", author="F. Scott Fitzgerald")
assert await client.model_with_namespace_on_properties_value.get() == model
await client.model_with_namespace_on_properties_value.put(model)


@pytest.mark.asyncio
async def test_xml_error_value(client: XmlClient, core_library):
with pytest.raises(core_library.exceptions.HttpResponseError) as ex:
Expand Down
Loading
Loading