diff --git a/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md b/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md new file mode 100644 index 00000000000..c4a7ac65651 --- /dev/null +++ b/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md @@ -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. diff --git a/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py b/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py index 27529cc5263..9004aa1640b 100644 --- a/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py +++ b/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py @@ -515,6 +515,186 @@ 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 = '/' + + 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 == "acontainer108f32e8" + 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 = '/' + + 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"}) + 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 == "acontainer" + 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 ). + # With , no matching children are found so both are None. + xml_payload = '/' + + 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 == "acontainer" + assert result.delimiter == "/" + assert isinstance(result.blobs, BlobsSegment) + # With , no or children exist → unwrapped empty lists stay None + assert result.blobs.blob_prefixes is None + 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 = '/' + + 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 == "acontainer" + assert result.delimiter == "/" + # unwrapped=True on a model field collects matching elements; 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): diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index ee93f122732..035891d5432 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -7265,6 +7265,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0",