Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
changeKind: fix
packages:
- "@typespec/http-client-python"
---
Return empty list instead of None for non-optional unwrapped XML list fields during deserialization
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-client-python"
---

Add unit test for deserializing Azure Blob Storage EnumerationResults XML payload with attributes, empty list element, and empty string element.
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,9 @@ class Model(_MyMutableMapping):
if len(items) > 0:
existed_attr_keys.append(xml_name)
dict_to_pass[rf._rest_name] = _deserialize(rf._type, items)
elif not rf._is_optional:
existed_attr_keys.append(xml_name)
dict_to_pass[rf._rest_name] = []
continue

# text element is primitive type
Expand Down Expand Up @@ -930,6 +933,8 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur
# is it optional?
try:
if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore
if rf:
rf._is_optional = True
if len(annotation.__args__) <= 2: # pyright: ignore
if_obj_deserializer = _get_deserialize_callable_from_annotation(
next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore
Expand Down Expand Up @@ -1115,6 +1120,7 @@ class _RestField:
self._is_discriminator = is_discriminator
self._visibility = visibility
self._is_model = False
self._is_optional = False
Comment thread
msyyc marked this conversation as resolved.
self._default = default
self._format = format
self._is_multipart_file_input = is_multipart_file_input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# ------------------------------------
import xml.etree.ElementTree as ET

from typing import Literal
from typing import Literal, Optional

from specialwords._utils.model_base import (
_get_element,
Expand Down Expand Up @@ -165,7 +165,7 @@ def __init__(self, *args, **kwargs):
_xml = {"name": "Data"}

result = _deserialize_xml(XmlModel, basic_xml)
assert result.age is None
assert result.age == []

def test_list_wrapped_items_name_basic_types(self):
"""Test XML list and wrap, items is basic type and there is itemsName."""
Expand Down Expand Up @@ -515,6 +515,248 @@ def __init__(self, *args, **kwargs):
assert isinstance(result.filter, CorrelationFilter)
assert result.filter.correlation_id == 12

def test_enumeration_results(self):
"""Test deserializing an Azure Blob Storage EnumerationResults XML payload."""
xml_payload = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://service.blob.core.windows.net/" ContainerName="my-container-108f32e8"><Delimiter>/</Delimiter><Blobs /><NextMarker /></EnumerationResults>'

class EnumerationResults(Model):
service_endpoint: str = rest_field(
name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
)
container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
blobs: list[str] = rest_field(name="Blobs", xml={"name": "Blobs"})
next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "EnumerationResults"}

result = _deserialize_xml(EnumerationResults, xml_payload)

assert result.service_endpoint == "https://service.blob.core.windows.net/"
assert result.container_name == "my-container-108f32e8"
assert result.delimiter == "/"
assert result.blobs == []
assert result.next_marker == ""

def test_enumeration_results_nested_empty_list(self):
"""Test deserializing XML where a container element holds a nested empty list (e.g. Blobs/BlobPrefixes)."""
xml_payload = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://service.blob.core.windows.net/" ContainerName="my-container"><Delimiter>/</Delimiter><Blobs><BlobPrefixes /></Blobs><NextMarker /></EnumerationResults>'

class BlobPrefix(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "BlobPrefix"}

class BlobsSegment(Model):
blob_prefixes: list[BlobPrefix] = rest_field(
name="BlobPrefixes", xml={"name": "BlobPrefixes", "itemsName": "BlobPrefix"}
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blobs"}

class EnumerationResults(Model):
service_endpoint: str = rest_field(
name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
)
container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
Comment thread
msyyc marked this conversation as resolved.
next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "EnumerationResults"}

result = _deserialize_xml(EnumerationResults, xml_payload)

assert result.service_endpoint == "https://service.blob.core.windows.net/"
assert result.container_name == "my-container"
assert result.delimiter == "/"
assert result.blobs.blob_prefixes == []
assert result.next_marker == ""

def test_enumeration_results_azure_sdk_pattern(self):
"""Test the real Azure SDK model pattern where BlobsSegment has two unwrapped list fields."""
# Both blob_prefixes and blob_items are unwrapped lists (items appear directly in <Blobs>).
# With <Blobs />, no matching children are found so both are None.
xml_payload = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://service.blob.core.windows.net/" ContainerName="my-container"><Delimiter>/</Delimiter><Blobs /><NextMarker /></EnumerationResults>'

class BlobPrefix(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "BlobPrefix"}

class BlobItem(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blob"}

class BlobsSegment(Model):
blob_prefixes: list[BlobPrefix] = rest_field(
name="blob_prefixes", xml={"name": "BlobPrefix", "unwrapped": True}
)
blob_items: list[BlobItem] = rest_field(name="blob_items", xml={"name": "Blob", "unwrapped": True})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blobs"}

class EnumerationResults(Model):
service_endpoint: str = rest_field(
name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
)
container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "EnumerationResults"}

result = _deserialize_xml(EnumerationResults, xml_payload)

assert result.service_endpoint == "https://service.blob.core.windows.net/"
assert result.container_name == "my-container"
assert result.delimiter == "/"
assert isinstance(result.blobs, BlobsSegment)
# With <Blobs />, no <BlobPrefix> or <Blob> children exist → unwrapped non-optional lists default to []
assert result.blobs.blob_prefixes == []
assert result.blobs.blob_items == []
assert result.next_marker == ""

def test_enumeration_results_azure_sdk_pattern_optional(self):
"""Test the Azure SDK pattern where unwrapped list fields are Optional[list[X]].

When the type is Optional[list[X]], empty unwrapped lists should stay None
(the element is absent, and None is a valid value for the optional type).
"""
xml_payload = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://service.blob.core.windows.net/" ContainerName="my-container"><Delimiter>/</Delimiter><Blobs /><NextMarker /></EnumerationResults>'

class BlobPrefix(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "BlobPrefix"}

class BlobItem(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blob"}

class BlobsSegment(Model):
blob_prefixes: Optional[list[BlobPrefix]] = rest_field(
name="blob_prefixes", xml={"name": "BlobPrefix", "unwrapped": True}
)
blob_items: Optional[list[BlobItem]] = rest_field(
name="blob_items", xml={"name": "Blob", "unwrapped": True}
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blobs"}

class EnumerationResults(Model):
service_endpoint: str = rest_field(
name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
)
container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "EnumerationResults"}

result = _deserialize_xml(EnumerationResults, xml_payload)

assert result.service_endpoint == "https://service.blob.core.windows.net/"
assert result.container_name == "my-container"
assert result.delimiter == "/"
assert isinstance(result.blobs, BlobsSegment)
# With <Blobs />, no <BlobPrefix> or <Blob> children exist → Optional lists stay None
assert result.blobs.blob_prefixes is None
Comment thread
msyyc marked this conversation as resolved.
assert result.blobs.blob_items is None
assert result.next_marker == ""

def test_enumeration_results_blobs_unwrapped(self):
"""Test what happens when the blobs field itself is declared with unwrapped=True."""
# When a non-list model field uses unwrapped=True, the matching XML elements are collected
# as a list and stored as-is (the field receives a list of ET.Element objects).
xml_payload = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://service.blob.core.windows.net/" ContainerName="my-container"><Delimiter>/</Delimiter><Blobs /><NextMarker /></EnumerationResults>'

class BlobPrefix(Model):
name: str = rest_field(name="Name", xml={"name": "Name"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "BlobPrefix"}

class BlobsSegment(Model):
blob_prefixes: list[BlobPrefix] = rest_field(
name="BlobPrefixes", xml={"name": "BlobPrefixes", "itemsName": "BlobPrefix"}
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "Blobs"}

class EnumerationResults(Model):
service_endpoint: str = rest_field(
name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
)
container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
# unwrapped=True on a model-typed field: the deserialization collects matching XML
# elements as a list (rather than deserializing them into the model).
blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs", "unwrapped": True})
next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

_xml = {"name": "EnumerationResults"}

result = _deserialize_xml(EnumerationResults, xml_payload)

assert result.service_endpoint == "https://service.blob.core.windows.net/"
assert result.container_name == "my-container"
assert result.delimiter == "/"
# unwrapped=True on a model field collects matching elements; <Blobs /> is found so it
# returns a list containing the raw ET.Element instead of a deserialized BlobsSegment.
assert isinstance(result.blobs, list)
assert len(result.blobs) == 1
assert isinstance(result.blobs[0], ET.Element)
assert result.next_marker == ""


class TestXmlSerialization:
def test_basic(self):
Expand Down
Loading