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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-python"
---

Fix enum member names derived from date-like TypeSpec labels (e.g. `` `2020-01-01` ``) being corrupted by the js-yaml (YAML 1.2) to PyYAML (YAML 1.1) boundary. String scalars are now force-quoted when serializing the code model so names such as `2020_01_01` round-trip as strings instead of being read back as integers
4 changes: 2 additions & 2 deletions packages/http-client-python/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
import { EmitContext, emitFile, joinPaths, NoTarget } from "@typespec/compiler";
import jsyaml from "js-yaml";
import pkgJson from "../../package.json" with { type: "json" };
import { emitCodeModel } from "./code-model.js";
import {
Expand All @@ -9,6 +8,7 @@ import {
PYGEN_WHEEL_FILENAME,
PYODIDE_VERSION,
} from "./constants.js";
import { dumpCodeModelToYaml } from "./external-process.js";
import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js";
import { runNodeEmit } from "./node-runner.js";
import { loadPyodide, PyodideInterface } from "./pyodide-loader.js";
Expand Down Expand Up @@ -250,7 +250,7 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
pyodide.FS.mkdirTree("/yaml");
pyodide.FS.mkdirTree("/output");
clearMemfsDirectory(pyodide, "/output");
pyodide.FS.writeFile(yamlFilePath, jsyaml.dump(parsedYamlMap));
pyodide.FS.writeFile(yamlFilePath, dumpCodeModelToYaml(parsedYamlMap));

await runPyodideGeneration(pyodide, "/output", yamlFilePath, commandArgs);
await copyPyodideOutputToHost(context, pyodide, "/output");
Expand Down
19 changes: 18 additions & 1 deletion packages/http-client-python/emitter/src/external-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ export function createTempPath(extension: string, prefix: string = "") {
return joinPaths(tspCodeGenTempDir, prefix + randomUUID() + extension);
}

/**
* Serialize the given codemodel to a YAML string.
*
* The generated YAML is consumed by the Python generator, which parses it with
* PyYAML (YAML 1.1). js-yaml, on the other hand, dumps using YAML 1.2 rules, so
* plain scalars such as `2020_01_01` (a snake-cased enum member name) are left
* unquoted because YAML 1.2 does not treat underscores as integer separators.
* PyYAML would then read `2020_01_01` back as the integer `20200101`, corrupting
* string values (e.g. enum member names, descriptions). Forcing every string
* scalar to be quoted guarantees that PyYAML round-trips them as strings.
* @param codemodel Codemodel to serialize
* @return the YAML representation of the codemodel.
*/
export function dumpCodeModelToYaml(codemodel: unknown): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wondering here do you actually even need yaml can't you serialize as json, would be faster and less deps

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like yaml is not a great format for data serialization better suited for config

return jsyaml.dump(codemodel, { forceQuotes: true, quotingType: '"' });
}

/**
* Save the given codemodel in a yaml file.
* @param name Name of the codemodel. To give a guide to the temp file name.
Expand All @@ -20,7 +37,7 @@ export function createTempPath(extension: string, prefix: string = "") {
export async function saveCodeModelAsYaml(name: string, codemodel: unknown): Promise<string> {
await mkdir(tspCodeGenTempDir, { recursive: true });
const filename = createTempPath(".yaml", name);
const yamlStr = jsyaml.dump(codemodel);
const yamlStr = dumpCodeModelToYaml(codemodel);
await writeFile(filename, yamlStr);
return filename;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ok, strictEqual } from "assert";
import { load } from "js-yaml";
import { describe, it } from "vitest";
import { dumpCodeModelToYaml } from "../src/external-process.js";

describe("typespec-python: external-process", () => {
// The Python generator parses the emitted YAML with PyYAML (YAML 1.1), where a plain
// scalar such as `2020_01_01` is interpreted as the integer `20200101`. js-yaml dumps
// using YAML 1.2 rules and would otherwise leave such string scalars unquoted, so we
// must force-quote strings to keep enum member names (and other string values) intact.
it("force-quotes string scalars that YAML 1.1 would misinterpret", () => {
const yaml = dumpCodeModelToYaml({ name: "2020_01_01" });
// The scalar must be quoted, otherwise PyYAML reads it back as the integer 20200101.
ok(yaml.includes('"2020_01_01"'), `expected the underscore scalar to be quoted, got: ${yaml}`);
ok(
!/name:\s*2020_01_01\s*$/m.test(yaml),
`expected no unquoted underscore scalar, got: ${yaml}`,
);
});

it("keeps string values as strings after a round-trip", () => {
const codeModel = { name: "2020_01_01", value: "2020-01-01", plain: "hello" };
const roundTripped = load(dumpCodeModelToYaml(codeModel)) as Record<string, unknown>;
strictEqual(roundTripped.name, "2020_01_01");
strictEqual(roundTripped.value, "2020-01-01");
strictEqual(roundTripped.plain, "hello");
});
});
Loading