Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/python-addExactSupport-2026-4-20-15-34-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/http-client-python"
---

add support for `exact` client names
4 changes: 3 additions & 1 deletion packages/http-client-python/emitter/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
camelToSnakeCase,
emitParamBase,
getAddedOn,
getClientName,
getDelimiterAndExplode,
getImplementation,
isAbstract,
Expand Down Expand Up @@ -459,6 +460,7 @@ function emitFlattenedParameter(
checkClientInput: false,
clientDefaultValue: null,
clientName: property.clientName,
isExactName: property.isExactName,
delimiter: null,
description: property.description,
implementation: "Method",
Expand Down Expand Up @@ -622,7 +624,7 @@ function emitHttpBodyParameter(
...emitParamBase(context, bodyParam, undefined, serviceApiVersions),
contentTypes: bodyParam.contentTypes,
location: bodyParam.kind,
clientName: bodyParam.isGeneratedName ? "body" : camelToSnakeCase(bodyParam.name),
clientName: bodyParam.isGeneratedName ? "body" : getClientName(bodyParam),
wireName: bodyParam.isGeneratedName ? "body" : bodyParam.name,
implementation: getImplementation(context, bodyParam),
clientDefaultValue: bodyParam.clientDefaultValue,
Expand Down
4 changes: 3 additions & 1 deletion packages/http-client-python/emitter/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
camelToSnakeCase,
emitParamBase,
getAddedOn,
getClientName,
getClientNamespace,
getImplementation,
} from "./utils.js";
Expand Down Expand Up @@ -233,7 +234,8 @@ function emitProperty(
addDisableGenerationMap(context, property.type);
}
return {
clientName: camelToSnakeCase(property.name),
clientName: getClientName(property),
isExactName: property.isExactName,
wireName:
(property.serializationOptions?.multipart
? property.serializationOptions?.multipart?.name
Expand Down
10 changes: 8 additions & 2 deletions packages/http-client-python/emitter/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -146,6 +150,7 @@ type ParamBase = {
description: string;
addedOn: string | undefined;
clientName: string;
isExactName: boolean;
inOverload: boolean;
isApiVersion: boolean;
type: Record<string, any>;
Expand Down Expand Up @@ -220,21 +225,22 @@ export function emitParamBase<TServiceOperation extends SdkServiceOperation>(
});
}
}
let clientName = camelToSnakeCase(parameter.name);
let clientName = getClientName(parameter);
if (
parameter.kind !== "method" &&
parameter.kind !== "credential" &&
parameter.kind !== "endpoint" &&
parameter.onClient &&
parameter.correspondingMethodParams[0]
) {
clientName = camelToSnakeCase(parameter.correspondingMethodParams[0].name);
clientName = getClientName(parameter.correspondingMethodParams[0]);
}
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading