From 20b8116f4fe88e1448c9e1f0ea7d8af51cd4bf5a Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 17 Nov 2025 11:39:44 +0800 Subject: [PATCH 1/7] [python] enable test case of discriminated union --- packages/http-client-python/eng/scripts/ci/regenerate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index b13ac4cd1da..406fbc3b5f8 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -22,7 +22,7 @@ const argv = parseArgs({ }); // Add this near the top with other constants -const SKIP_SPECS = ["type/union/discriminated"]; +const SKIP_SPECS = []; // Get the directory of the current file const PLUGIN_DIR = argv.values.pluginDir @@ -272,6 +272,10 @@ const EMITTER_OPTIONS: Record | Record Date: Tue, 18 Nov 2025 09:26:08 +0000 Subject: [PATCH 2/7] fix ci --- .../pygen/codegen/models/operation.py | 2 ++ .../codegen/serializers/builder_serializer.py | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index f9e5e623825..8751dae7e68 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -408,6 +408,8 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements file_import.merge(self.get_request_builder_import(self.request_builder, async_mode, serialize_namespace)) if self.overloads: file_import.add_submodule_import("typing", "overload", ImportType.STDLIB) + for overload in self.overloads: + file_import.merge(overload.imports(async_mode, **kwargs)) if self.code_model.options["models-mode"] == "dpg": relative_path = self.code_model.get_relative_import_path( serialize_namespace, module_name="_utils.model_base" 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 ae12128a3b1..a1f8a3b42fd 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 @@ -776,8 +776,14 @@ def _initialize_overloads(self, builder: OperationType, is_paging: bool = False) client_names = [ overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads ] - for v in sorted(set(client_names), key=client_names.index): - retval.append(f"_{v} = None") + all_dpg_model_overloads = False + if self.code_model.options["models-mode"] == "dpg": + all_dpg_model_overloads = all( + isinstance(o.parameters.body_parameter.type, DPGModelType) for o in builder.overloads + ) + if not all_dpg_model_overloads: + for v in sorted(set(client_names), key=client_names.index): + retval.append(f"_{v} = None") try: # if there is a binary overload, we do a binary check first. binary_overload = cast( @@ -803,17 +809,20 @@ def _initialize_overloads(self, builder: OperationType, is_paging: bool = False) f'"{other_overload.parameters.body_parameter.default_content_type}"{check_body_suffix}' ) except StopIteration: - for idx, overload in enumerate(builder.overloads): - if_statement = "if" if idx == 0 else "elif" - body_param = overload.parameters.body_parameter - retval.append( - f"{if_statement} {body_param.type.instance_check_template.format(body_param.client_name)}:" - ) - if body_param.default_content_type and not same_content_type: + if all_dpg_model_overloads: + retval.extend(f"{l}" for l in self._create_body_parameter(cast(OperationType, builder.overloads[0]))) + else: + for idx, overload in enumerate(builder.overloads): + if_statement = "if" if idx == 0 else "elif" + body_param = overload.parameters.body_parameter retval.append( - f' content_type = content_type or "{body_param.default_content_type}"{check_body_suffix}' + f"{if_statement} {body_param.type.instance_check_template.format(body_param.client_name)}:" ) - retval.extend(f" {l}" for l in self._create_body_parameter(cast(OperationType, overload))) + if body_param.default_content_type and not same_content_type: + retval.append( + f' content_type = content_type or "{body_param.default_content_type}"{check_body_suffix}' + ) + retval.extend(f" {l}" for l in self._create_body_parameter(cast(OperationType, overload))) return retval def _create_request_builder_call( From c4ac97a92784cee524f294fa94db74d787a50af0 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 18 Nov 2025 09:42:22 +0000 Subject: [PATCH 3/7] fix ci --- .../generator/pygen/codegen/models/operation.py | 3 ++- .../generator/pygen/codegen/serializers/builder_serializer.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index 8751dae7e68..c8f729e1f02 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -409,7 +409,8 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements if self.overloads: file_import.add_submodule_import("typing", "overload", ImportType.STDLIB) for overload in self.overloads: - file_import.merge(overload.imports(async_mode, **kwargs)) + if overload.parameters.has_body: + file_import.merge(overload.parameters.body_parameter.type.imports(**kwargs)) if self.code_model.options["models-mode"] == "dpg": relative_path = self.code_model.get_relative_import_path( serialize_namespace, module_name="_utils.model_base" 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 a1f8a3b42fd..248a7165092 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 @@ -777,7 +777,7 @@ def _initialize_overloads(self, builder: OperationType, is_paging: bool = False) overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads ] all_dpg_model_overloads = False - if self.code_model.options["models-mode"] == "dpg": + if self.code_model.options["models-mode"] == "dpg" and builder.overloads: all_dpg_model_overloads = all( isinstance(o.parameters.body_parameter.type, DPGModelType) for o in builder.overloads ) From 7a776a05b0a45b514978f7a36617fe6f9486c167 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 18 Nov 2025 09:43:27 +0000 Subject: [PATCH 4/7] add changelog --- .../python-discriminated-union-2025-10-18-9-43-10.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/python-discriminated-union-2025-10-18-9-43-10.md diff --git a/.chronus/changes/python-discriminated-union-2025-10-18-9-43-10.md b/.chronus/changes/python-discriminated-union-2025-10-18-9-43-10.md new file mode 100644 index 00000000000..d0db15882ee --- /dev/null +++ b/.chronus/changes/python-discriminated-union-2025-10-18-9-43-10.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Fix import when body parameter is union of models \ No newline at end of file From 5b6789eab0ce02f268602e82a507084bc44d1f0a Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 25 Dec 2025 03:23:57 +0000 Subject: [PATCH 5/7] update test case --- packages/http-client-python/eng/scripts/ci/regenerate.ts | 2 +- .../http-client-python/generator/test/azure/requirements.txt | 1 + .../generator/test/unbranded/requirements.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 8a53441f339..8c219ace4f9 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -22,7 +22,7 @@ const argv = parseArgs({ }); // Add this near the top with other constants -const SKIP_SPECS = []; +const SKIP_SPECS: string[] = []; // Get the directory of the current file const PLUGIN_DIR = argv.values.pluginDir diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index b672290928d..a07314cb8b0 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -92,6 +92,7 @@ azure-mgmt-core==1.6.0 -e ./generated/typetest-property-additionalproperties -e ./generated/typetest-scalar -e ./generated/typetest-union +-e ./generated/typetest-union-discriminated -e ./generated/typetest-model-empty -e ./generated/headasbooleantrue -e ./generated/headasbooleanfalse diff --git a/packages/http-client-python/generator/test/unbranded/requirements.txt b/packages/http-client-python/generator/test/unbranded/requirements.txt index c2facfcc4e5..6f05e8a108b 100644 --- a/packages/http-client-python/generator/test/unbranded/requirements.txt +++ b/packages/http-client-python/generator/test/unbranded/requirements.txt @@ -41,6 +41,7 @@ -e ./generated/typetest-property-additionalproperties -e ./generated/typetest-scalar -e ./generated/typetest-union +-e ./generated/typetest-union-discriminated -e ./generated/typetest-model-empty -e ./generated/headasbooleantrue -e ./generated/headasbooleanfalse From 037d335391b5f9367bf72925112c6c3032817ed8 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 25 Dec 2025 03:24:16 +0000 Subject: [PATCH 6/7] add test case --- .../test_typetest_union_discriminated.py | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 packages/http-client-python/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py b/packages/http-client-python/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py new file mode 100644 index 00000000000..154541195b7 --- /dev/null +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py @@ -0,0 +1,290 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +from typetest.union.discriminated import DiscriminatedClient +from typetest.union.discriminated import models + + +@pytest.fixture +def client(): + with DiscriminatedClient() as client: + yield client + + +@pytest.fixture +def cat_body(): + """Cat model for testing.""" + return models.Cat(name="Whiskers", meow=True) + + +@pytest.fixture +def dog_body(): + """Dog model for testing.""" + return models.Dog(name="Rex", bark=False) + + +# Tests for No Envelope / Default (inline discriminator with "kind") +@pytest.mark.skip(reason="After completely support discriminated unions, enable these tests") +class TestNoEnvelopeDefault: + """Test discriminated union with inline discriminator (no envelope).""" + + def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with default (no query param or kind=cat). + + Expected response: + { + "kind": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.default.get() + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_kind_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with kind=cat query parameter. + + Expected response: + { + "kind": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.default.get(kind="cat") + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_kind_dog(self, client: DiscriminatedClient, dog_body: models.Dog): + """Test getting dog with kind=dog query parameter. + + Expected response: + { + "kind": "dog", + "name": "Rex", + "bark": false + } + """ + result = client.no_envelope.default.get(kind="dog") + assert result == dog_body + assert isinstance(result, models.Dog) + + def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test sending cat with inline discriminator. + + Expected request: + { + "kind": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.default.put(cat_body) + assert result == cat_body + assert isinstance(result, models.Cat) + + +# Tests for No Envelope / Custom Discriminator (inline with custom "type" property) +@pytest.mark.skip(reason="After completely support discriminated unions, enable these tests") +class TestNoEnvelopeCustomDiscriminator: + """Test discriminated union with inline discriminator and custom discriminator property name.""" + + def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with default (no query param or type=cat). + + Expected response: + { + "type": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.custom_discriminator.get() + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_type_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with type=cat query parameter. + + Expected response: + { + "type": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.custom_discriminator.get(type="cat") + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_type_dog(self, client: DiscriminatedClient, dog_body: models.Dog): + """Test getting dog with type=dog query parameter. + + Expected response: + { + "type": "dog", + "name": "Rex", + "bark": false + } + """ + result = client.no_envelope.custom_discriminator.get(type="dog") + assert result == dog_body + assert isinstance(result, models.Dog) + + def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test sending cat with inline custom discriminator. + + Expected request: + { + "type": "cat", + "name": "Whiskers", + "meow": true + } + """ + result = client.no_envelope.custom_discriminator.put(cat_body) + assert result == cat_body + assert isinstance(result, models.Cat) + + +# Tests for Envelope / Object / Default (envelope with "kind" and "value") +@pytest.mark.skip(reason="After completely support discriminated unions, enable these tests") +class TestEnvelopeObjectDefault: + """Test discriminated union with default envelope serialization.""" + + def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with default (no query param or kind=cat). + + Expected response: + { + "kind": "cat", + "value": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.default.get() + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_kind_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with kind=cat query parameter. + + Expected response: + { + "kind": "cat", + "value": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.default.get(kind="cat") + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_kind_dog(self, client: DiscriminatedClient, dog_body: models.Dog): + """Test getting dog with kind=dog query parameter. + + Expected response: + { + "kind": "dog", + "value": { + "name": "Rex", + "bark": false + } + } + """ + result = client.envelope.object.default.get(kind="dog") + assert result == dog_body + assert isinstance(result, models.Dog) + + def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test sending cat with envelope serialization. + + Expected request: + { + "kind": "cat", + "value": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.default.put(cat_body) + assert result == cat_body + assert isinstance(result, models.Cat) + + +# Tests for Envelope / Object / Custom Properties (envelope with custom "petType" and "petData") +@pytest.mark.skip(reason="After completely support discriminated unions, enable these tests") +class TestEnvelopeObjectCustomProperties: + """Test discriminated union with custom property names in envelope.""" + + def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with default (no query param or petType=cat). + + Expected response: + { + "petType": "cat", + "petData": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.custom_properties.get() + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_pet_type_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test getting cat with petType=cat query parameter. + + Expected response: + { + "petType": "cat", + "petData": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.custom_properties.get(pet_type="cat") + assert result == cat_body + assert isinstance(result, models.Cat) + + def test_get_with_pet_type_dog(self, client: DiscriminatedClient, dog_body: models.Dog): + """Test getting dog with petType=dog query parameter. + + Expected response: + { + "petType": "dog", + "petData": { + "name": "Rex", + "bark": false + } + } + """ + result = client.envelope.object.custom_properties.get(pet_type="dog") + assert result == dog_body + assert isinstance(result, models.Dog) + + def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat): + """Test sending cat with custom property names in envelope. + + Expected request: + { + "petType": "cat", + "petData": { + "name": "Whiskers", + "meow": true + } + } + """ + result = client.envelope.object.custom_properties.put(cat_body) + assert result == cat_body + assert isinstance(result, models.Cat) From 43fc049fb67b70d3cea9899064d4e7fb3c79620c Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 25 Dec 2025 05:06:23 +0000 Subject: [PATCH 7/7] fix ci --- packages/http-client-python/eng/scripts/ci/regenerate.ts | 4 ++-- .../http-client-python/generator/test/azure/requirements.txt | 2 +- .../test_typetest_union_discriminated.py | 4 ++-- .../generator/test/unbranded/requirements.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 8c219ace4f9..85a4a729eac 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -276,8 +276,8 @@ const EMITTER_OPTIONS: Record | Record