From 4707d228714764d7d756cf06d7997418649e826c Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 14:44:23 -0500 Subject: [PATCH 1/9] capture xml name --- packages/http-client-python/emitter/src/http.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 58170b69a17..e87e8b1f113 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -142,7 +142,15 @@ function getWireNameFromPropertySegments( if (segments[0].kind === "property") { return segments .filter((s) => s.kind === "property") - .map((s) => s.serializationOptions.json?.name ?? "") + .map((s) => { + if (s.serializationOptions.json) { + return s.serializationOptions.json.name; + } + if (s.serializationOptions.xml) { + return s.serializationOptions.xml.name; + } + return ""; + }) .join("."); } From eb6232334f6ff963201342a7b47bf09e1000fd2d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 15:10:01 -0500 Subject: [PATCH 2/9] add initial code for xml paging --- .../pygen/codegen/models/paging_operation.py | 10 +++++++++ .../codegen/serializers/builder_serializer.py | 22 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py index 9a744a7ad14..2830b5abddd 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py @@ -72,6 +72,13 @@ def has_continuation_token(self) -> bool: @property def next_variable_name(self) -> str: return "_continuation_token" if self.has_continuation_token else "next_link" + + @property + def is_xml_paging(self) -> bool: + try: + return self.responses[0].item_type.xml_metadata is not None + except KeyError: + return False def _get_attr_name(self, wire_name: str) -> str: response_type = self.responses[0].type @@ -176,6 +183,9 @@ def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import.merge(self.item_type.imports(**kwargs)) if self.default_error_deserialization(serialize_namespace) or self.need_deserialize: file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL) + if self.is_xml_paging: + file_import.add_submodule_import("xml.etree", "ElementTree", ImportType.STDLIB, alias="ET") + file_import.add_submodule_import(relative_path, "_convert_element", ImportType.LOCAL) return file_import diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index fefe97f878a..5a8e938e075 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1375,7 +1375,10 @@ def _function_def(self) -> str: def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # pylint: disable=too-many-statements retval = [f"{'async ' if self.async_mode else ''}def extract_data(pipeline_response):"] response = builder.responses[0] - deserialized = "pipeline_response.http_response.json()" + if builder.is_xml_paging: + deserialized = "ET.fromstring(pipeline_response.http_response.text())" + else: + deserialized = "pipeline_response.http_response.json()" if self.code_model.options["models-mode"] == "msrest": suffix = ".http_response" if hasattr(builder, "initial_operation") else "" deserialize_type = response.serialization_type(serialize_namespace=self.serialize_namespace) @@ -1395,6 +1398,10 @@ def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # item_name = builder.item_name if self.code_model.options["models-mode"] == "msrest": access = f".{item_name}" + elif builder.is_xml_paging: + # For XML, use .find() to navigate the element tree + item_name_array = item_name.split(".") + access = "".join([f'.find("{i}")' for i in item_name_array]) else: item_name_array = item_name.split(".") access = ( @@ -1412,11 +1419,17 @@ def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # retval.append(" if cls:") retval.append(" list_of_elem = cls(list_of_elem) # type: ignore") + cont_token_expr: Optional[str] = None # For XML, we need to extract find() result first if builder.has_continuation_token: location = builder.continuation_token.get("output", {}).get("location") wire_name = builder.continuation_token.get("output", {}).get("wireName") or "" if location == "header": cont_token_property = f'pipeline_response.http_response.headers.get("{wire_name}") or None' + elif builder.is_xml_paging: + wire_name_array = wire_name.split(".") + wire_name_call = "".join([f'.find("{i}")' for i in wire_name_array]) + cont_token_expr = f"deserialized{wire_name_call}" + cont_token_property = "_cont_token_elem.text if _cont_token_elem is not None else None" else: wire_name_array = wire_name.split(".") wire_name_call = ( @@ -1429,6 +1442,11 @@ def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # cont_token_property = "None" elif self.code_model.options["models-mode"] == "msrest": cont_token_property = f"deserialized.{next_link_name} or None" + elif builder.is_xml_paging: + next_link_name_array = next_link_name.split(".") + access = "".join([f'.find("{i}")' for i in next_link_name_array]) + cont_token_expr = f"deserialized{access}" + cont_token_property = "_cont_token_elem.text if _cont_token_elem is not None else None" elif builder.next_link_is_nested: next_link_name_array = next_link_name.split(".") access = ( @@ -1439,6 +1457,8 @@ def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # else: cont_token_property = f'deserialized.get("{next_link_name}") or None' list_type = "AsyncList" if self.async_mode else "iter" + if cont_token_expr: + retval.append(f" _cont_token_elem = {cont_token_expr}") retval.append(f" return {cont_token_property}, {list_type}(list_of_elem)") return retval From fb364feeef687616d56ab994915e3910f4e48fb9 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 15:44:25 -0500 Subject: [PATCH 3/9] add changeset --- .../changes/python-storagePagingFix-2026-0-23-15-44-19.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/python-storagePagingFix-2026-0-23-15-44-19.md diff --git a/.chronus/changes/python-storagePagingFix-2026-0-23-15-44-19.md b/.chronus/changes/python-storagePagingFix-2026-0-23-15-44-19.md new file mode 100644 index 00000000000..e8363a868f1 --- /dev/null +++ b/.chronus/changes/python-storagePagingFix-2026-0-23-15-44-19.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +Add support for xml paging \ No newline at end of file From 54d3ec05df35b8d63eb210e73157e385b47d7506 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 15:55:21 -0500 Subject: [PATCH 4/9] add tests for paging --- .../asynctests/test_payload_pageable_async.py | 6 ++++++ .../test/generic_mock_api_tests/test_payload_pageable.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py index e1ac8a4d655..f5bb3326dd3 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py @@ -113,3 +113,9 @@ async def test_request_header_nested_response_body(client: PageableClient): async def test_list_without_continuation(client: PageableClient): result = [p async for p in client.page_size.list_without_continuation()] assert_result(result) + + +@pytest.mark.asyncio +async def test_xml_pagination_list(client: PageableClient): + result = [p async for p in client.xml_pagination.list()] + assert_result(result) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py index d07bd3ac047..0f6fecc3ac7 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py @@ -81,3 +81,8 @@ def test_request_header_nested_response_body(client: PageableClient): def test_list_without_continuation(client: PageableClient): result = list(client.page_size.list_without_continuation()) assert_result(result) + + +def test_xml_pagination_list(client: PageableClient): + result = list(client.xml_pagination.list()) + assert_result(result) From 70279f0c27a0b1d70442ef4e30f483b4bd364a3a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 15:57:06 -0500 Subject: [PATCH 5/9] fix lint --- .../generator/pygen/codegen/models/paging_operation.py | 2 +- .../generator/pygen/codegen/serializers/builder_serializer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py index 2830b5abddd..97a9c825249 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py @@ -72,7 +72,7 @@ def has_continuation_token(self) -> bool: @property def next_variable_name(self) -> str: return "_continuation_token" if self.has_continuation_token else "next_link" - + @property def is_xml_paging(self) -> bool: try: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 5a8e938e075..924f16b297a 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1372,7 +1372,7 @@ def _prepare_request_callback(self, builder: PagingOperationType) -> list[str]: def _function_def(self) -> str: return "def" - def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # pylint: disable=too-many-statements + def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # pylint: disable=too-many-statements,too-many-branches retval = [f"{'async ' if self.async_mode else ''}def extract_data(pipeline_response):"] response = builder.responses[0] if builder.is_xml_paging: From 20ed2e2486951db50eb397b24d16db8acfcbbab1 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 23 Jan 2026 17:00:33 -0500 Subject: [PATCH 6/9] update to next link tests --- .../asynctests/test_payload_pageable_async.py | 4 ++-- .../test/generic_mock_api_tests/test_payload_pageable.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py index f5bb3326dd3..04e533d355a 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py @@ -116,6 +116,6 @@ async def test_list_without_continuation(client: PageableClient): @pytest.mark.asyncio -async def test_xml_pagination_list(client: PageableClient): - result = [p async for p in client.xml_pagination.list()] +async def test_xml_pagination_list_with_next_link(client: PageableClient): + result = [p async for p in client.xml_pagination.list_with_next_link()] assert_result(result) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py index 0f6fecc3ac7..d60c5d58acd 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py @@ -83,6 +83,6 @@ def test_list_without_continuation(client: PageableClient): assert_result(result) -def test_xml_pagination_list(client: PageableClient): - result = list(client.xml_pagination.list()) +def test_xml_pagination_list_with_next_link(client: PageableClient): + result = list(client.xml_pagination.list_with_next_link()) assert_result(result) From 5a29069a991b8f74488e004d5ebb518a7730e7a4 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 26 Jan 2026 11:13:02 +0800 Subject: [PATCH 7/9] fix ci --- .../asynctests/test_payload_pageable_async.py | 9 +++++---- .../test/generic_mock_api_tests/test_payload_pageable.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py index 04e533d355a..45c5d12de49 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_payload_pageable_async.py @@ -115,7 +115,8 @@ async def test_list_without_continuation(client: PageableClient): assert_result(result) -@pytest.mark.asyncio -async def test_xml_pagination_list_with_next_link(client: PageableClient): - result = [p async for p in client.xml_pagination.list_with_next_link()] - assert_result(result) +# after https://github.com/microsoft/typespec/pull/9455 released, we could enable this test again +# @pytest.mark.asyncio +# async def test_xml_pagination_list_with_next_link(client: PageableClient): +# result = [p async for p in client.xml_pagination.list_with_next_link()] +# assert_result(result) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py index d60c5d58acd..1302cb8b957 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/test_payload_pageable.py @@ -83,6 +83,7 @@ def test_list_without_continuation(client: PageableClient): assert_result(result) -def test_xml_pagination_list_with_next_link(client: PageableClient): - result = list(client.xml_pagination.list_with_next_link()) - assert_result(result) +# # after https://github.com/microsoft/typespec/pull/9455 released, we could enable this test again +# def test_xml_pagination_list_with_next_link(client: PageableClient): +# result = list(client.xml_pagination.list_with_next_link()) +# assert_result(result) From e620d1c67424ea85eb23d94703f101e5136f9553 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 26 Jan 2026 12:55:24 +0800 Subject: [PATCH 8/9] fix judgement for xml paging --- .../generator/pygen/codegen/models/paging_operation.py | 2 +- .../generator/pygen/codegen/serializers/builder_serializer.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py index 97a9c825249..bba3a904f1c 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py @@ -76,7 +76,7 @@ def next_variable_name(self) -> str: @property def is_xml_paging(self) -> bool: try: - return self.responses[0].item_type.xml_metadata is not None + return bool(self.responses[0].item_type.xml_metadata) except KeyError: return False diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 924f16b297a..883d3d2aa39 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1372,7 +1372,9 @@ def _prepare_request_callback(self, builder: PagingOperationType) -> list[str]: def _function_def(self) -> str: return "def" - def _extract_data_callback(self, builder: PagingOperationType) -> list[str]: # pylint: disable=too-many-statements,too-many-branches + def _extract_data_callback( + self, builder: PagingOperationType + ) -> list[str]: # pylint: disable=too-many-statements,too-many-branches retval = [f"{'async ' if self.async_mode else ''}def extract_data(pipeline_response):"] response = builder.responses[0] if builder.is_xml_paging: From 23e127a8e09c9934e619afb86e2544c389afe4ed Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 26 Jan 2026 13:05:05 +0800 Subject: [PATCH 9/9] fix lint issue --- .../generator/pygen/codegen/serializers/builder_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 883d3d2aa39..35c4215b03f 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1372,9 +1372,9 @@ def _prepare_request_callback(self, builder: PagingOperationType) -> list[str]: def _function_def(self) -> str: return "def" - def _extract_data_callback( + def _extract_data_callback( # pylint: disable=too-many-statements,too-many-branches self, builder: PagingOperationType - ) -> list[str]: # pylint: disable=too-many-statements,too-many-branches + ) -> list[str]: retval = [f"{'async ' if self.async_mode else ''}def extract_data(pipeline_response):"] response = builder.responses[0] if builder.is_xml_paging: