From 02eb6d0e46889e747b005bffe088cd234f0fc08d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 20 May 2026 15:30:41 -0400 Subject: [PATCH 1/3] add support for exact client name --- .../http-client-python/emitter/src/http.ts | 7 +++++- .../http-client-python/emitter/src/types.ts | 3 ++- .../http-client-python/emitter/src/utils.ts | 8 +++++-- .../pygen/codegen/models/operation.py | 7 +++++- .../pygen/codegen/models/parameter.py | 1 + .../codegen/serializers/builder_serializer.py | 5 +++-- .../generator/pygen/preprocess/__init__.py | 22 ++++++++++++++----- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 07104c972ef..6fbb8eb386e 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -459,6 +459,7 @@ function emitFlattenedParameter( checkClientInput: false, clientDefaultValue: null, clientName: property.clientName, + isExactName: property.isExactName, delimiter: null, description: property.description, implementation: "Method", @@ -622,7 +623,11 @@ function emitHttpBodyParameter( ...emitParamBase(context, bodyParam, undefined, serviceApiVersions), contentTypes: bodyParam.contentTypes, location: bodyParam.kind, - clientName: bodyParam.isGeneratedName ? "body" : camelToSnakeCase(bodyParam.name), + clientName: bodyParam.isGeneratedName + ? "body" + : bodyParam.isExactName + ? bodyParam.name + : camelToSnakeCase(bodyParam.name), wireName: bodyParam.isGeneratedName ? "body" : bodyParam.name, implementation: getImplementation(context, bodyParam), clientDefaultValue: bodyParam.clientDefaultValue, diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index 89f35478abe..c9ba2de1081 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -233,7 +233,8 @@ function emitProperty( addDisableGenerationMap(context, property.type); } return { - clientName: camelToSnakeCase(property.name), + clientName: property.isExactName ? property.name : camelToSnakeCase(property.name), + isExactName: property.isExactName, wireName: (property.serializationOptions?.multipart ? property.serializationOptions?.multipart?.name diff --git a/packages/http-client-python/emitter/src/utils.ts b/packages/http-client-python/emitter/src/utils.ts index 9b30231a71d..65c527877e7 100644 --- a/packages/http-client-python/emitter/src/utils.ts +++ b/packages/http-client-python/emitter/src/utils.ts @@ -146,6 +146,7 @@ type ParamBase = { description: string; addedOn: string | undefined; clientName: string; + isExactName: boolean; inOverload: boolean; isApiVersion: boolean; type: Record; @@ -220,7 +221,7 @@ export function emitParamBase( }); } } - let clientName = camelToSnakeCase(parameter.name); + let clientName = parameter.isExactName ? parameter.name : camelToSnakeCase(parameter.name); if ( parameter.kind !== "method" && parameter.kind !== "credential" && @@ -228,13 +229,16 @@ export function emitParamBase( parameter.onClient && parameter.correspondingMethodParams[0] ) { - clientName = camelToSnakeCase(parameter.correspondingMethodParams[0].name); + clientName = parameter.correspondingMethodParams[0].isExactName + ? parameter.correspondingMethodParams[0].name + : camelToSnakeCase(parameter.correspondingMethodParams[0].name); } return { optional: parameter.optional, description: (parameter.summary ? parameter.summary : parameter.doc) ?? "", addedOn: getAddedOn(context, parameter, serviceApiVersions), clientName, + isExactName: parameter.isExactName, inOverload: false, isApiVersion: parameter.isApiVersionParam, isContinuationToken: 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 c5f15593893..38e94483ed5 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -91,11 +91,16 @@ def __init__( self.has_etag: bool = self.yaml_data.get("hasEtag", False) self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId") + @property + def exact_name_params(self) -> set[str]: + """Return the set of client names for parameters with isExactName.""" + return {p.client_name for p in self.parameters.method if p.is_exact_name} + @property def stream_value(self) -> Union[str, bool]: return ( f'kwargs.pop("stream", {self.has_stream_response})' - if self.expose_stream_keyword and self.has_response_body + if self.expose_stream_keyword and self.has_response_body and "stream" not in self.exact_name_params else self.has_stream_response ) diff --git a/packages/http-client-python/generator/pygen/codegen/models/parameter.py b/packages/http-client-python/generator/pygen/codegen/models/parameter.py index 5abf03a5e73..13a8c95d0c0 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/parameter.py +++ b/packages/http-client-python/generator/pygen/codegen/models/parameter.py @@ -87,6 +87,7 @@ def __init__( self.default_to_unset_sentinel: bool = self.yaml_data.get("defaultToUnsetSentinel", False) self.hide_in_method: bool = self.yaml_data.get("hideInMethod", False) self.is_continuation_token: bool = bool(self.yaml_data.get("isContinuationToken")) + self.is_exact_name: bool = self.yaml_data.get("isExactName", False) def get_declaration(self, value: Any = None) -> Any: return self.type.get_declaration(value) 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 e43dcd916eb..5dcf94e32b4 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 @@ -627,7 +627,8 @@ def _api_version_validation(self, builder: OperationType) -> str: return "" def pop_kwargs_from_signature(self, builder: OperationType) -> list[str]: - kwargs_to_pop = builder.parameters.kwargs_to_pop + exact_names = builder.exact_name_params + kwargs_to_pop = [k for k in builder.parameters.kwargs_to_pop if k.client_name not in exact_names] kwargs = self.parameter_serializer.pop_kwargs_from_signature( kwargs_to_pop, check_kwarg_dict=True, @@ -645,7 +646,7 @@ def pop_kwargs_from_signature(self, builder: OperationType) -> list[str]: body_parameter=builder.parameters.body_parameter if builder.parameters.has_body else None, ) for p in builder.parameters.parameters: - if p.hide_in_operation_signature and not p.is_continuation_token: + if p.hide_in_operation_signature and not p.is_continuation_token and p.client_name not in exact_names: kwargs.append(f'{p.client_name} = kwargs.pop("{p.client_name}", None)') cls_annotation = builder.cls_type_annotation( async_mode=self.async_mode, serialize_namespace=self.serialize_namespace diff --git a/packages/http-client-python/generator/pygen/preprocess/__init__.py b/packages/http-client-python/generator/pygen/preprocess/__init__.py index bc595e1748a..d883f52cf31 100644 --- a/packages/http-client-python/generator/pygen/preprocess/__init__.py +++ b/packages/http-client-python/generator/pygen/preprocess/__init__.py @@ -264,9 +264,10 @@ def update_types(self, yaml_data: list[dict[str, Any]]) -> None: for type in yaml_data: for property in type.get("properties", []): property["description"] = update_description(property.get("description", "")) - property["clientName"] = self.pad_reserved_words( - property["clientName"].lower(), PadType.PROPERTY, property - ) + if not property.get("isExactName", False): + property["clientName"] = self.pad_reserved_words( + property["clientName"].lower(), PadType.PROPERTY, property + ) add_redefined_builtin_info(property["clientName"], property) if type.get("name"): pad_type = PadType.MODEL if type["type"] == "model" else PadType.ENUM_CLASS @@ -362,14 +363,25 @@ def get_operation_updater(self, yaml_data: dict[str, Any]) -> Callable[[dict[str def update_parameter(self, yaml_data: dict[str, Any]) -> None: yaml_data["description"] = update_description(yaml_data.get("description", "")) - if not (yaml_data["location"] == "header" and yaml_data["clientName"] in ("content_type", "accept")): + if not yaml_data.get("isExactName", False) and not ( + yaml_data["location"] == "header" and yaml_data["clientName"] in ("content_type", "accept") + ): yaml_data["clientName"] = self.pad_reserved_words( yaml_data["clientName"].lower(), PadType.PARAMETER, yaml_data ) if yaml_data.get("propertyToParameterName"): # need to create a new one with padded values (but NOT keys, since keys are wire names) + # build a lookup of exact-name properties from the body type's properties + exact_name_props = set() + for prop in yaml_data.get("type", {}).get("properties", []): + if prop.get("isExactName", False): + exact_name_props.add(prop.get("wireName", "")) yaml_data["propertyToParameterName"] = { - prop: self.pad_reserved_words(param_name, PadType.PARAMETER, yaml_data).lower() + prop: ( + param_name + if prop in exact_name_props + else self.pad_reserved_words(param_name, PadType.PARAMETER, yaml_data).lower() + ) for prop, param_name in yaml_data["propertyToParameterName"].items() } wire_name_lower = (yaml_data.get("wireName") or "").lower() From 6d93fa56b4d5491b2288a0de29028f948f337144 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 20 May 2026 15:34:14 -0400 Subject: [PATCH 2/3] add changeset --- .../changes/python-addExactSupport-2026-4-20-15-34-6.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/python-addExactSupport-2026-4-20-15-34-6.md diff --git a/.chronus/changes/python-addExactSupport-2026-4-20-15-34-6.md b/.chronus/changes/python-addExactSupport-2026-4-20-15-34-6.md new file mode 100644 index 00000000000..4bca945985b --- /dev/null +++ b/.chronus/changes/python-addExactSupport-2026-4-20-15-34-6.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +add support for `exact` client names \ No newline at end of file From 96c92fb0d6b7e139a77c329a36370cf76be16ec6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 21 May 2026 14:49:50 -0400 Subject: [PATCH 3/3] refactor: extract getClientName helper for isExactName logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-python/emitter/src/http.ts | 7 ++----- packages/http-client-python/emitter/src/types.ts | 3 ++- packages/http-client-python/emitter/src/utils.ts | 10 ++++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 6fbb8eb386e..7dac2bf3eb6 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -29,6 +29,7 @@ import { camelToSnakeCase, emitParamBase, getAddedOn, + getClientName, getDelimiterAndExplode, getImplementation, isAbstract, @@ -623,11 +624,7 @@ function emitHttpBodyParameter( ...emitParamBase(context, bodyParam, undefined, serviceApiVersions), contentTypes: bodyParam.contentTypes, location: bodyParam.kind, - clientName: bodyParam.isGeneratedName - ? "body" - : bodyParam.isExactName - ? bodyParam.name - : camelToSnakeCase(bodyParam.name), + clientName: bodyParam.isGeneratedName ? "body" : getClientName(bodyParam), wireName: bodyParam.isGeneratedName ? "body" : bodyParam.name, implementation: getImplementation(context, bodyParam), clientDefaultValue: bodyParam.clientDefaultValue, diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index c9ba2de1081..825c56b063b 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -24,6 +24,7 @@ import { camelToSnakeCase, emitParamBase, getAddedOn, + getClientName, getClientNamespace, getImplementation, } from "./utils.js"; @@ -233,7 +234,7 @@ function emitProperty( addDisableGenerationMap(context, property.type); } return { - clientName: property.isExactName ? property.name : camelToSnakeCase(property.name), + clientName: getClientName(property), isExactName: property.isExactName, wireName: (property.serializationOptions?.multipart diff --git a/packages/http-client-python/emitter/src/utils.ts b/packages/http-client-python/emitter/src/utils.ts index 65c527877e7..31d819015ed 100644 --- a/packages/http-client-python/emitter/src/utils.ts +++ b/packages/http-client-python/emitter/src/utils.ts @@ -107,6 +107,10 @@ export function camelToSnakeCase(name: string): string { return result_final; } +export function getClientName(named: { name: string; isExactName: boolean }): string { + return named.isExactName ? named.name : camelToSnakeCase(named.name); +} + export function getImplementation( context: PythonSdkContext, parameter: SdkEndpointParameter | SdkCredentialParameter | SdkMethodParameter | SdkHttpParameter, @@ -221,7 +225,7 @@ export function emitParamBase( }); } } - let clientName = parameter.isExactName ? parameter.name : camelToSnakeCase(parameter.name); + let clientName = getClientName(parameter); if ( parameter.kind !== "method" && parameter.kind !== "credential" && @@ -229,9 +233,7 @@ export function emitParamBase( parameter.onClient && parameter.correspondingMethodParams[0] ) { - clientName = parameter.correspondingMethodParams[0].isExactName - ? parameter.correspondingMethodParams[0].name - : camelToSnakeCase(parameter.correspondingMethodParams[0].name); + clientName = getClientName(parameter.correspondingMethodParams[0]); } return { optional: parameter.optional,