From 2ba823c56c6b2a53866acdb65d8e0e795f58b1b2 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 20 Oct 2025 14:27:49 +0800 Subject: [PATCH 01/10] support @alternateType --- .../codegen/serializers/general_serializer.py | 2 +- .../codegen/templates/model_base.py.jinja2 | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index cebe5d86166..46039e89cc8 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -23,7 +23,7 @@ "msrest": "0.7.1", "isodate": "0.6.1", "azure-mgmt-core": "1.6.0", - "azure-core": "1.35.0", + "azure-core": "1.36.0", "typing-extensions": "4.6.0", "corehttp": "1.0.0b6", } diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 3158f8c46e6..1d8132c8c2d 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -25,6 +25,9 @@ from {{ code_model.core_library }}.exceptions import DeserializationError from {{ code_model.core_library }}{{ "" if code_model.is_azure_flavor else ".utils" }} import CaseInsensitiveEnumMeta from {{ code_model.core_library }}.{{ "" if code_model.is_azure_flavor else "runtime." }}pipeline import PipelineResponse from {{ code_model.core_library }}.serialization import _Null +{% if code_model.is_azure_flavor %} +from {{ code_model.core_library }}.serialization import TypeHandlerRegistry +{% endif %} from {{ code_model.core_library }}.rest import HttpResponse _LOGGER = logging.getLogger(__name__) @@ -34,6 +37,10 @@ __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] TZ_UTC = timezone.utc _T = typing.TypeVar("_T") +{% if code_model.is_azure_flavor %} +TYPE_HANDLER_REGISTRY = TypeHandlerRegistry() +{% endif %} + def _timedelta_as_isostr(td: timedelta) -> str: """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' @@ -158,6 +165,11 @@ class SdkJSONEncoder(JSONEncoder): except AttributeError: # This will be raised when it hits value.total_seconds in the method above pass + {% if code_model.is_azure_flavor %} + custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o) + if custom_serializer: + return custom_serializer(o) + {% endif %} return super(SdkJSONEncoder, self).default(o) @@ -507,6 +519,14 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m except AttributeError: # This will be raised when it hits value.total_seconds in the method above pass + + {% if code_model.is_azure_flavor %} + # Check if there's a custom serializer for the type + custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o) + if custom_serializer: + return custom_serializer(o) + {% endif %} + return o @@ -888,6 +908,12 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur if get_deserializer(annotation, rf): return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) + {% if code_model.is_azure_flavor %} + deserializer = TYPE_HANDLER_REGISTRY.get_deserializer(annotation) + if deserializer: + return deserializer + {% endif %} + return functools.partial(_deserialize_default, annotation) From ba2e48c0e2870d4998b23aecd4829d5efb13d6c1 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 20 Oct 2025 14:28:52 +0800 Subject: [PATCH 02/10] add changelog --- .../changes/external-type-python-2025-9-20-14-28-41.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/external-type-python-2025-9-20-14-28-41.md diff --git a/.chronus/changes/external-type-python-2025-9-20-14-28-41.md b/.chronus/changes/external-type-python-2025-9-20-14-28-41.md new file mode 100644 index 00000000000..2977eb159e2 --- /dev/null +++ b/.chronus/changes/external-type-python-2025-9-20-14-28-41.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +Support SDK users defined customized serialization/deserialization function for external models \ No newline at end of file From b136ce8aadf86e69860a2b2b590c221651ecded8 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 20 Oct 2025 16:25:32 +0800 Subject: [PATCH 03/10] support external type --- .../http-client-python/emitter/src/types.ts | 6 ++++ .../pygen/codegen/models/__init__.py | 2 ++ .../pygen/codegen/models/code_model.py | 9 ++++++ .../pygen/codegen/models/primitive_types.py | 28 +++++++++++++++++++ .../codegen/serializers/general_serializer.py | 11 ++++++++ .../codegen/templates/model_base.py.jinja2 | 10 +++---- .../packaging_templates/pyproject.toml.jinja2 | 3 ++ 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index 26bee78d935..a2dff68a3c2 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -270,6 +270,12 @@ function emitModel(context: PythonSdkContext, type: SdkModelType): Record[] = []; const newValue = { type: type.kind, diff --git a/packages/http-client-python/generator/pygen/codegen/models/__init__.py b/packages/http-client-python/generator/pygen/codegen/models/__init__.py index 119a1b68efd..68fd4d845dc 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/models/__init__.py @@ -31,6 +31,7 @@ SdkCoreType, DecimalType, MultiPartFileType, + ExternalType ) from .enum_type import EnumType, EnumValue from .base import BaseType @@ -151,6 +152,7 @@ "credential": StringType, "sdkcore": SdkCoreType, "multipartfile": MultiPartFileType, + "external": ExternalType, } _LOGGER = logging.getLogger(__name__) diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 6d2796d49e2..11a7bfc1b6e 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -10,6 +10,7 @@ from .enum_type import EnumType from .model_type import ModelType, UsageFlags from .combined_type import CombinedType +from .primitive_types import ExternalType from .client import Client from .request_builder import RequestBuilder, OverloadedRequestBuilder from .operation_group import OperationGroup @@ -101,6 +102,9 @@ def __init__( self._operations_folder_name: dict[str, str] = {} self._relative_import_path: dict[str, str] = {} self.metadata: dict[str, Any] = yaml_data.get("metadata", {}) + self.has_external_type = any( + isinstance(t, ExternalType) for t in self.types_map.values() + ) @staticmethod def get_imported_namespace_for_client(imported_namespace: str, async_mode: bool = False) -> str: @@ -488,3 +492,8 @@ def _get_relative_generation_dir(self, root_dir: Path, namespace: str) -> Path: @property def has_operation_named_list(self) -> bool: return any(o.name.lower() == "list" for c in self.clients for og in c.operation_groups for o in og.operations) + + @property + def external_types(self) -> list[ExternalType]: + """All of the external types""" + return [t for t in self.types_map.values() if isinstance(t, ExternalType)] diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index 00b480c5537..c9cf02b754d 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -614,6 +614,34 @@ def instance_check_template(self) -> str: def serialization_type(self, **kwargs: Any) -> str: return self.name +class ExternalType(PrimitiveType): + def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: + super().__init__(yaml_data=yaml_data, code_model=code_model) + self.external_type_info = yaml_data.get("externalTypeInfo", {}) + self.identity = self.external_type_info.get("identity", "") + self.submodule = ".".join(self.identity.split(".")[:-1]) + self.min_version = self.external_type_info.get("minVersion", "") + self.package_name = self.external_type_info.get("package", "") + + def docstring_type(self, **kwargs: Any) -> str: + return f"~{self.identity}" + + def type_annotation(self, **kwargs: Any) -> str: + is_operation_file = kwargs.get("is_operation_file", False) + return self.identity if is_operation_file else f'"{self.identity}' + + def imports(self, **kwargs: Any) -> FileImport: + file_import = super().imports(**kwargs) + file_import.add_import(self.submodule, ImportType.THIRDPARTY, TypingSection.REGULAR) + return file_import + + @property + def instance_check_template(self) -> str: + return f"isinstance({{}}, {self.identity})" + + def serialization_type(self, **kwargs: Any) -> str: + return self.identity + class MultiPartFileType(PrimitiveType): def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index 46039e89cc8..b53c50903c4 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -126,6 +126,16 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs dev_status = "4 - Beta" else: dev_status = "5 - Production/Stable" + + additional_dependencies = [] + if self.code_model.has_external_type: + for item in self.code_model.external_types: + if item.package_name: + if item.min_version: + additional_dependencies.append(f'"{item.package_name}>={item.min_version}"') + else: + additional_dependencies.append(f'"{item.package_name}"') + params |= { "code_model": self.code_model, "dev_status": dev_status, @@ -136,6 +146,7 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs "VERSION_MAP": VERSION_MAP, "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, + "ADDITIONAL_DEPENDENCIES": additional_dependencies, } params |= {"options": self.code_model.options} params |= kwargs diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 1d8132c8c2d..9a8879cef90 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -25,7 +25,7 @@ from {{ code_model.core_library }}.exceptions import DeserializationError from {{ code_model.core_library }}{{ "" if code_model.is_azure_flavor else ".utils" }} import CaseInsensitiveEnumMeta from {{ code_model.core_library }}.{{ "" if code_model.is_azure_flavor else "runtime." }}pipeline import PipelineResponse from {{ code_model.core_library }}.serialization import _Null -{% if code_model.is_azure_flavor %} +{% if code_model.has_external_type %} from {{ code_model.core_library }}.serialization import TypeHandlerRegistry {% endif %} from {{ code_model.core_library }}.rest import HttpResponse @@ -37,7 +37,7 @@ __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] TZ_UTC = timezone.utc _T = typing.TypeVar("_T") -{% if code_model.is_azure_flavor %} +{% if code_model.has_external_type %} TYPE_HANDLER_REGISTRY = TypeHandlerRegistry() {% endif %} @@ -165,7 +165,7 @@ class SdkJSONEncoder(JSONEncoder): except AttributeError: # This will be raised when it hits value.total_seconds in the method above pass - {% if code_model.is_azure_flavor %} + {% if code_model.has_external_type %} custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o) if custom_serializer: return custom_serializer(o) @@ -520,7 +520,7 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m # This will be raised when it hits value.total_seconds in the method above pass - {% if code_model.is_azure_flavor %} + {% if code_model.has_external_type %} # Check if there's a custom serializer for the type custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o) if custom_serializer: @@ -908,7 +908,7 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur if get_deserializer(annotation, rf): return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) - {% if code_model.is_azure_flavor %} + {% if code_model.has_external_type %} deserializer = TYPE_HANDLER_REGISTRY.get_deserializer(annotation) if deserializer: return deserializer diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 index 0f52b35fd17..de9e1b4b26e 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 @@ -56,6 +56,9 @@ dependencies = [ "{{ dep }}", {% endfor %} {% endif %} + {% for dep in ADDITIONAL_DEPENDENCIES %} + {{ dep }}, + {% endfor %} ] dynamic = [ {% if options.get('package-mode') %}"version", {% endif %}"readme" From 41ef0505d591b6d252ae276d5b66401a05f24461 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 20 Oct 2025 17:36:34 +0800 Subject: [PATCH 04/10] fix type cache --- .../http-client-python/emitter/src/types.ts | 4 +- .../pygen/codegen/models/primitive_types.py | 2 +- .../codegen/serializers/general_serializer.py | 46 +++++++++++-------- .../packaging_templates/pyproject.toml.jinja2 | 2 +- .../packaging_templates/setup.py.jinja2 | 3 ++ 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index a2dff68a3c2..ee7a373ed8f 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -271,10 +271,10 @@ function emitModel(context: PythonSdkContext, type: SdkModelType): Record[] = []; const newValue = { diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index c9cf02b754d..c70b0d6ad18 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -628,7 +628,7 @@ def docstring_type(self, **kwargs: Any) -> str: def type_annotation(self, **kwargs: Any) -> str: is_operation_file = kwargs.get("is_operation_file", False) - return self.identity if is_operation_file else f'"{self.identity}' + return self.identity if is_operation_file else f'"{self.identity}"' def imports(self, **kwargs: Any) -> FileImport: file_import = super().imports(**kwargs) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index b53c50903c4..3ae0787adc6 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -57,7 +57,17 @@ def _extract_min_dependency(self, s): m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s) return parse_version(m.group(1)) if m else parse_version("0") - def _keep_pyproject_fields(self, file_content: str) -> dict: + + def _update_version_map(self, version_map: dict[str, str], dep_name: str, dep: str) -> None: + # For tracked dependencies, check if the version is higher than our default + default_version = parse_version(version_map[dep_name]) + dep_version = self._extract_min_dependency(dep) + # If the version is higher than the default, update VERSION_MAP + # with higher min dependency version + if dep_version > default_version: + version_map[dep_name] = str(dep_version) + + def _keep_pyproject_fields(self, file_content: str, additional_version_map: dict[str, str]) -> dict: # Load the pyproject.toml file if it exists and extract fields to keep. result: dict = {"KEEP_FIELDS": {}} try: @@ -80,15 +90,11 @@ def _keep_pyproject_fields(self, file_content: str) -> dict: for dep in loaded_pyproject_toml["project"]["dependencies"]: dep_name = re.split(r"[<>=\[]", dep)[0].strip() - # Check if dependency is one we track in VERSION_MAP + # Check if dependency is one we track in version map if dep_name in VERSION_MAP: - # For tracked dependencies, check if the version is higher than our default - default_version = parse_version(VERSION_MAP[dep_name]) - dep_version = self._extract_min_dependency(dep) - # If the version is higher than the default, update VERSION_MAP - # with higher min dependency version - if dep_version > default_version: - VERSION_MAP[dep_name] = str(dep_version) + self._update_version_map(VERSION_MAP, dep_name, dep) + elif dep_name in additional_version_map: + self._update_version_map(additional_version_map, dep_name, dep) else: # Keep non-default dependencies kept_deps.append(dep) @@ -107,9 +113,18 @@ def _keep_pyproject_fields(self, file_content: str) -> dict: def serialize_package_file(self, template_name: str, file_content: str, **kwargs: Any) -> str: template = self.env.get_template(template_name) + additional_version_map = {} + if self.code_model.has_external_type: + for item in self.code_model.external_types: + if item.package_name: + if item.min_version: + additional_version_map[item.package_name] = item.min_version + else: + additional_version_map[item.package_name] = "0" + # Add fields to keep from an existing pyproject.toml if template_name == "pyproject.toml.jinja2": - params = self._keep_pyproject_fields(file_content) + params = self._keep_pyproject_fields(file_content, additional_version_map) else: params = {} @@ -127,14 +142,7 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs else: dev_status = "5 - Production/Stable" - additional_dependencies = [] - if self.code_model.has_external_type: - for item in self.code_model.external_types: - if item.package_name: - if item.min_version: - additional_dependencies.append(f'"{item.package_name}>={item.min_version}"') - else: - additional_dependencies.append(f'"{item.package_name}"') + params |= { "code_model": self.code_model, @@ -146,7 +154,7 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs "VERSION_MAP": VERSION_MAP, "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, - "ADDITIONAL_DEPENDENCIES": additional_dependencies, + "ADDITIONAL_DEPENDENCIES": [f'{item[0]}>={item[1]}' for item in additional_version_map.items()], } params |= {"options": self.code_model.options} params |= kwargs diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 index de9e1b4b26e..4b6d74796c5 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 @@ -57,7 +57,7 @@ dependencies = [ {% endfor %} {% endif %} {% for dep in ADDITIONAL_DEPENDENCIES %} - {{ dep }}, + "{{ dep }}", {% endfor %} ] dynamic = [ diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/setup.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/setup.py.jinja2 index 2590ce72776..396d915e0e9 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/setup.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/setup.py.jinja2 @@ -108,6 +108,9 @@ setup( "corehttp[requests]>={{ VERSION_MAP["corehttp"] }}", {% endif %} "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}", + {% for dep in ADDITIONAL_DEPENDENCIES %} + {{ dep }}, + {% endfor %} ], {% if options["package-mode"] %} python_requires=">={{ MIN_PYTHON_VERSION }}", From 17919d7dce582d4e7be569d4391a413f95cc2fc7 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 20 Oct 2025 17:39:11 +0800 Subject: [PATCH 05/10] format --- .../generator/pygen/codegen/models/__init__.py | 2 +- .../pygen/codegen/models/code_model.py | 4 +--- .../pygen/codegen/models/primitive_types.py | 3 ++- .../codegen/serializers/general_serializer.py | 17 +++++++---------- .../generator/test/azure/requirements.txt | 1 + 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/__init__.py b/packages/http-client-python/generator/pygen/codegen/models/__init__.py index 68fd4d845dc..a1d9f9a4dbc 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/models/__init__.py @@ -31,7 +31,7 @@ SdkCoreType, DecimalType, MultiPartFileType, - ExternalType + ExternalType, ) from .enum_type import EnumType, EnumValue from .base import BaseType diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 11a7bfc1b6e..86927cac2ea 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -102,9 +102,7 @@ def __init__( self._operations_folder_name: dict[str, str] = {} self._relative_import_path: dict[str, str] = {} self.metadata: dict[str, Any] = yaml_data.get("metadata", {}) - self.has_external_type = any( - isinstance(t, ExternalType) for t in self.types_map.values() - ) + self.has_external_type = any(isinstance(t, ExternalType) for t in self.types_map.values()) @staticmethod def get_imported_namespace_for_client(imported_namespace: str, async_mode: bool = False) -> str: diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index c70b0d6ad18..604c3feb5b3 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -614,6 +614,7 @@ def instance_check_template(self) -> str: def serialization_type(self, **kwargs: Any) -> str: return self.name + class ExternalType(PrimitiveType): def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: super().__init__(yaml_data=yaml_data, code_model=code_model) @@ -622,7 +623,7 @@ def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: self.submodule = ".".join(self.identity.split(".")[:-1]) self.min_version = self.external_type_info.get("minVersion", "") self.package_name = self.external_type_info.get("package", "") - + def docstring_type(self, **kwargs: Any) -> str: return f"~{self.identity}" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index 3ae0787adc6..fad14ccee31 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -57,7 +57,6 @@ def _extract_min_dependency(self, s): m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s) return parse_version(m.group(1)) if m else parse_version("0") - def _update_version_map(self, version_map: dict[str, str], dep_name: str, dep: str) -> None: # For tracked dependencies, check if the version is higher than our default default_version = parse_version(version_map[dep_name]) @@ -115,12 +114,12 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs additional_version_map = {} if self.code_model.has_external_type: - for item in self.code_model.external_types: - if item.package_name: - if item.min_version: - additional_version_map[item.package_name] = item.min_version - else: - additional_version_map[item.package_name] = "0" + for item in self.code_model.external_types: + if item.package_name: + if item.min_version: + additional_version_map[item.package_name] = item.min_version + else: + additional_version_map[item.package_name] = "0" # Add fields to keep from an existing pyproject.toml if template_name == "pyproject.toml.jinja2": @@ -141,9 +140,7 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs dev_status = "4 - Beta" else: dev_status = "5 - Production/Stable" - - params |= { "code_model": self.code_model, "dev_status": dev_status, @@ -154,7 +151,7 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs "VERSION_MAP": VERSION_MAP, "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, - "ADDITIONAL_DEPENDENCIES": [f'{item[0]}>={item[1]}' for item in additional_version_map.items()], + "ADDITIONAL_DEPENDENCIES": [f"{item[0]}>={item[1]}" for item in additional_version_map.items()], } params |= {"options": self.code_model.options} params |= kwargs diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 9bd7d045774..d7f33efcfdf 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -14,6 +14,7 @@ azure-mgmt-core==1.6.0 -e ./generated/azure-client-generator-core-usage -e ./generated/azure-client-generator-core-override -e ./generated/azure-client-generator-core-client-location +-e ./generated/azure-client-generator-core-alternate-type -e ./generated/azure-core-basic -e ./generated/azure-core-scalar -e ./generated/azure-core-lro-rpc From df66fce420131b19493a34ee6bbb86fd7e297ec4 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 21 Oct 2025 02:14:09 +0000 Subject: [PATCH 06/10] update --- .../pygen/codegen/templates/model_base.py.jinja2 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 9a8879cef90..8d9fbf2b825 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -325,7 +325,13 @@ def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = return _deserialize_int_as_str if rf and rf._format: return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) + {% if code_model.has_external_type %} + if _DESERIALIZE_MAPPING.get(annotation): + return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore + return TYPE_HANDLER_REGISTRY.get_deserializer(annotation) # pyright: ignore + {% else %} return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore + {% endif %} def _get_type_alias_type(module_name: str, alias_name: str): @@ -908,12 +914,6 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur if get_deserializer(annotation, rf): return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) - {% if code_model.has_external_type %} - deserializer = TYPE_HANDLER_REGISTRY.get_deserializer(annotation) - if deserializer: - return deserializer - {% endif %} - return functools.partial(_deserialize_default, annotation) From e640a3c7360f68577664efcb2e1b977138e7d3df Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 21 Oct 2025 06:30:06 +0000 Subject: [PATCH 07/10] fix for annotation --- .../generator/pygen/codegen/models/primitive_types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index 604c3feb5b3..aaa275345a7 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -628,8 +628,7 @@ def docstring_type(self, **kwargs: Any) -> str: return f"~{self.identity}" def type_annotation(self, **kwargs: Any) -> str: - is_operation_file = kwargs.get("is_operation_file", False) - return self.identity if is_operation_file else f'"{self.identity}"' + return self.identity def imports(self, **kwargs: Any) -> FileImport: file_import = super().imports(**kwargs) From a9940995dd423d7c571f2e1b008256422d3741c9 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 21 Oct 2025 06:50:51 +0000 Subject: [PATCH 08/10] update --- .../generator/pygen/codegen/models/primitive_types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index aaa275345a7..20332fcd4a9 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -642,6 +642,11 @@ def instance_check_template(self) -> str: def serialization_type(self, **kwargs: Any) -> str: return self.identity + @property + def default_template_representation_declaration(self) -> str: + value = f"{self.identity}(...)" + return f'"{value}"' if self.code_model.for_test else value + class MultiPartFileType(PrimitiveType): def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: From f11c4d3d037533a45ba14641a72121ef9f4a056f Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 2 Dec 2025 10:28:53 +0800 Subject: [PATCH 09/10] fix comments --- .../generator/pygen/codegen/models/primitive_types.py | 8 ++++---- .../pygen/codegen/serializers/general_serializer.py | 6 ++++-- .../pygen/codegen/templates/model_base.py.jinja2 | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index 20332fcd4a9..6cf5271c336 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -618,11 +618,11 @@ def serialization_type(self, **kwargs: Any) -> str: class ExternalType(PrimitiveType): def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: super().__init__(yaml_data=yaml_data, code_model=code_model) - self.external_type_info = yaml_data.get("externalTypeInfo", {}) - self.identity = self.external_type_info.get("identity", "") + external_type_info = yaml_data.get("externalTypeInfo", {}) + self.identity = external_type_info.get("identity", "") self.submodule = ".".join(self.identity.split(".")[:-1]) - self.min_version = self.external_type_info.get("minVersion", "") - self.package_name = self.external_type_info.get("package", "") + self.min_version = external_type_info.get("minVersion", "") + self.package_name = external_type_info.get("package", "") def docstring_type(self, **kwargs: Any) -> str: return f"~{self.identity}" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index fad14ccee31..25029f30ac1 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -66,7 +66,7 @@ def _update_version_map(self, version_map: dict[str, str], dep_name: str, dep: s if dep_version > default_version: version_map[dep_name] = str(dep_version) - def _keep_pyproject_fields(self, file_content: str, additional_version_map: dict[str, str]) -> dict: + def external_lib_version_map(self, file_content: str, additional_version_map: dict[str, str]) -> dict: # Load the pyproject.toml file if it exists and extract fields to keep. result: dict = {"KEEP_FIELDS": {}} try: @@ -119,11 +119,13 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs if item.min_version: additional_version_map[item.package_name] = item.min_version else: + # Use "0" as a placeholder when min_version is not specified for external types. + # This allows the dependency to be included without a specific version constraint. additional_version_map[item.package_name] = "0" # Add fields to keep from an existing pyproject.toml if template_name == "pyproject.toml.jinja2": - params = self._keep_pyproject_fields(file_content, additional_version_map) + params = self.external_lib_version_map(file_content, additional_version_map) else: params = {} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 8d9fbf2b825..d732e7d4e27 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -532,7 +532,6 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m if custom_serializer: return custom_serializer(o) {% endif %} - return o From d197fc63d19a641f1304c2cdeaed90ae49045e8f Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 2 Dec 2025 06:54:43 +0000 Subject: [PATCH 10/10] update --- .../generator/pygen/codegen/templates/model_base.py.jinja2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index d732e7d4e27..4a830e1dfcd 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -326,7 +326,7 @@ def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = if rf and rf._format: return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) {% if code_model.has_external_type %} - if _DESERIALIZE_MAPPING.get(annotation): + if _DESERIALIZE_MAPPING.get(annotation): # pyright: ignore return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore return TYPE_HANDLER_REGISTRY.get_deserializer(annotation) # pyright: ignore {% else %} @@ -525,12 +525,13 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m except AttributeError: # This will be raised when it hits value.total_seconds in the method above pass - {% if code_model.has_external_type %} + # Check if there's a custom serializer for the type custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o) if custom_serializer: return custom_serializer(o) + {% endif %} return o