diff --git a/.chronus/changes/fix-python-wire-encoding-2026-08-11.md b/.chronus/changes/fix-python-wire-encoding-2026-08-11.md new file mode 100644 index 00000000000..55fca413747 --- /dev/null +++ b/.chronus/changes/fix-python-wire-encoding-2026-08-11.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Preserve Python boolean, integer, and bytes client types when using supported string, base64, or base64url wire encodings. diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index 617f26f62cf..4b099e05c5c 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -474,10 +474,17 @@ const sdkScalarKindToPythonKind: Record = { azureLocation: "string", }; +const supportedPythonEncodings = new Map>([ + ["boolean", new Set(["string"])], + ["integer", new Set(["string"])], + ["bytes", new Set(["base64", "base64url"])], +]); + function emitBuiltInType( context: PythonSdkContext, type: SdkBuiltInType | SdkDurationType | SdkDateTimeType, ): Record { + const pythonType = sdkScalarKindToPythonKind[type.kind] || type.kind; if (type.encode) { if (type.kind === "duration") { if (type.encode === "ISO8601") { @@ -509,7 +516,14 @@ function emitBuiltInType( } } - // fallback to wire type for unknown or unsupported encode + if (supportedPythonEncodings.get(pythonType)?.has(type.encode)) { + return getSimpleTypeResult(context, { + type: pythonType, + encode: type.encode, + }); + } + + // Python cannot apply unknown/custom encodings, so expose the wire type instead of silently ignoring the encoding. if ("wireType" in type && type.wireType !== undefined) { return getSimpleTypeResult(context, { type: sdkScalarKindToPythonKind[type.wireType.kind] || type.wireType.kind, @@ -519,7 +533,7 @@ function emitBuiltInType( } return getSimpleTypeResult(context, { - type: sdkScalarKindToPythonKind[type.kind] || type.kind, // TODO: switch to kind + type: pythonType, // TODO: switch to kind encode: type.encode, }); } diff --git a/packages/http-client-python/emitter/test/types.test.ts b/packages/http-client-python/emitter/test/types.test.ts new file mode 100644 index 00000000000..750c357a670 --- /dev/null +++ b/packages/http-client-python/emitter/test/types.test.ts @@ -0,0 +1,72 @@ +import type { SdkBuiltInType } from "@azure-tools/typespec-client-generator-core"; +import { deepStrictEqual } from "assert"; +import { describe, it } from "vitest"; +import type { PythonSdkContext } from "../src/lib.js"; +import { getType } from "../src/types.js"; + +function createBuiltInType( + kind: SdkBuiltInType["kind"], + encode?: string, + wireType?: SdkBuiltInType, +): SdkBuiltInType { + return { + kind, + name: kind, + crossLanguageDefinitionId: `TypeSpec.${kind}`, + decorators: [], + encode, + wireType, + }; +} + +function emitBuiltInType(type: SdkBuiltInType): Record { + const context = { + __simpleTypesMap: new Map(), + } as PythonSdkContext; + return getType(context, type); +} + +describe("typespec-python: built-in types", () => { + it("preserves boolean encoded as string", () => { + deepStrictEqual( + emitBuiltInType(createBuiltInType("boolean", "string", createBuiltInType("string"))), + { + type: "boolean", + encode: "string", + }, + ); + }); + + it.each(["safeint", "uint32", "uint8"] as const)( + "preserves %s encoded as string as an integer", + (kind) => { + deepStrictEqual( + emitBuiltInType(createBuiltInType(kind, "string", createBuiltInType("string"))), + { + type: "integer", + encode: "string", + }, + ); + }, + ); + + it.each(["base64", "base64url"] as const)("preserves bytes encoded as %s", (encode) => { + deepStrictEqual( + emitBuiltInType(createBuiltInType("bytes", encode, createBuiltInType("string"))), + { + type: "bytes", + encode, + }, + ); + }); + + it("falls back to the wire type for a custom encoding", () => { + deepStrictEqual( + emitBuiltInType(createBuiltInType("string", "abc", createBuiltInType("int32"))), + { + type: "integer", + encode: "abc", + }, + ); + }); +});