From e58dbf57b38fd9f4182164e32855a71744066c80 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 2 Sep 2026 09:22:30 +0000 Subject: [PATCH 01/11] Fix Python no-client warning suppression --- ...n-no-sdk-clients-suppression-2026-09-02.md | 7 ++++++ .../http-client-python/emitter/src/emitter.ts | 12 ++++++---- .../http-client-python/emitter/src/lib.ts | 2 +- .../emitter/test/emitter.test.ts | 24 +++++++++++++++++++ 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .chronus/changes/fix-python-no-sdk-clients-suppression-2026-09-02.md create mode 100644 packages/http-client-python/emitter/test/emitter.test.ts diff --git a/.chronus/changes/fix-python-no-sdk-clients-suppression-2026-09-02.md b/.chronus/changes/fix-python-no-sdk-clients-suppression-2026-09-02.md new file mode 100644 index 00000000000..fdde2184628 --- /dev/null +++ b/.chronus/changes/fix-python-no-sdk-clients-suppression-2026-09-02.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Allow `no-sdk-clients` warnings to be suppressed by reporting them on the TypeSpec service namespace. \ No newline at end of file diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index ab9bd654a08..10b1ad84850 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,6 +1,6 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; -import type { EmitContext } from "@typespec/compiler"; -import { emitFile, joinPaths, NoTarget } from "@typespec/compiler"; +import type { EmitContext, Namespace, Program } from "@typespec/compiler"; +import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; import { @@ -128,6 +128,10 @@ async function runPyodideGeneration( await pyodide.runPythonAsync(pyodideGenerationCode, { globals }); } +export function getNoSdkClientsDiagnosticTarget(program: Program): Namespace { + return listServices(program)[0].type; +} + async function copyPyodideOutputToHost( context: EmitContext, pyodide: PyodideInterface, @@ -185,9 +189,9 @@ async function onEmitMain(context: EmitContext) { if (sdkContext.sdkPackage.clients.length === 0) { reportDiagnostic(program, { code: "no-sdk-clients", - target: NoTarget, + target: getNoSdkClientsDiagnosticTarget(program), }); - return; + // return; } const resolvedOptions = sdkContext.emitContext.options; diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index 047db866e28..740e79be11f 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -180,7 +180,7 @@ const libDef = { }, }, "no-sdk-clients": { - severity: "error", + severity: "warning", messages: { default: "The Python emitter did not find any SDK clients in this TypeSpec program. The current Python generator expects at least one client/service to generate code.", diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts new file mode 100644 index 00000000000..a9cc79f2bfc --- /dev/null +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -0,0 +1,24 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTester, t } from "@typespec/compiler/testing"; +import { strictEqual } from "assert"; +import { it } from "vitest"; +import { getNoSdkClientsDiagnosticTarget } from "../src/emitter.js"; +import { reportDiagnostic } from "../src/lib.js"; + +const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); + +it("targets the service namespace when no SDK clients are found", async () => { + const { Service, program } = await Tester.compile(t.code` + #suppress "@typespec/http-client-python/no-sdk-clients" "This service intentionally has no client." + @service namespace ${t.namespace("Service")} {} + `); + + const target = getNoSdkClientsDiagnosticTarget(program); + strictEqual(target, Service); + + reportDiagnostic(program, { code: "no-sdk-clients", target }); + strictEqual( + program.diagnostics.some((x) => x.code.endsWith("/no-sdk-clients")), + false, + ); +}); From e44febc33babba382d20104df0c30f52d87a52df Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 2 Sep 2026 09:33:41 +0000 Subject: [PATCH 02/11] Address no-client warning review feedback --- .../http-client-python/emitter/src/emitter.ts | 7 +++---- .../emitter/test/emitter.test.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 10b1ad84850..0c2e6f3f0eb 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,5 +1,5 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; -import type { EmitContext, Namespace, Program } from "@typespec/compiler"; +import type { DiagnosticTarget, EmitContext, Program } from "@typespec/compiler"; import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; @@ -128,8 +128,8 @@ async function runPyodideGeneration( await pyodide.runPythonAsync(pyodideGenerationCode, { globals }); } -export function getNoSdkClientsDiagnosticTarget(program: Program): Namespace { - return listServices(program)[0].type; +export function getNoSdkClientsDiagnosticTarget(program: Program): DiagnosticTarget { + return listServices(program)[0]?.type ?? NoTarget; } async function copyPyodideOutputToHost( @@ -191,7 +191,6 @@ async function onEmitMain(context: EmitContext) { code: "no-sdk-clients", target: getNoSdkClientsDiagnosticTarget(program), }); - // return; } const resolvedOptions = sdkContext.emitContext.options; diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index a9cc79f2bfc..16f63c7ecd5 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -1,4 +1,4 @@ -import { resolvePath } from "@typespec/compiler"; +import { NoTarget, resolvePath } from "@typespec/compiler"; import { createTester, t } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; import { it } from "vitest"; @@ -22,3 +22,16 @@ it("targets the service namespace when no SDK clients are found", async () => { false, ); }); + +it("uses no target when no service exists", async () => { + const { program } = await Tester.compile(`model Widget {}`); + + const target = getNoSdkClientsDiagnosticTarget(program); + strictEqual(target, NoTarget); + + reportDiagnostic(program, { code: "no-sdk-clients", target }); + strictEqual( + program.diagnostics.some((x) => x.code.endsWith("/no-sdk-clients")), + true, + ); +}); From 7fab8793235e613f2a4de7e3fc484e042387706f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 11:58:19 -0400 Subject: [PATCH 03/11] refactor(emitter): inline no-client diagnostic target Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/http-client-python/emitter/src/emitter.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 0c2e6f3f0eb..2057fe3106e 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,5 +1,5 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; -import type { DiagnosticTarget, EmitContext, Program } from "@typespec/compiler"; +import type { EmitContext, Program } from "@typespec/compiler"; import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; @@ -128,10 +128,6 @@ async function runPyodideGeneration( await pyodide.runPythonAsync(pyodideGenerationCode, { globals }); } -export function getNoSdkClientsDiagnosticTarget(program: Program): DiagnosticTarget { - return listServices(program)[0]?.type ?? NoTarget; -} - async function copyPyodideOutputToHost( context: EmitContext, pyodide: PyodideInterface, @@ -189,7 +185,7 @@ async function onEmitMain(context: EmitContext) { if (sdkContext.sdkPackage.clients.length === 0) { reportDiagnostic(program, { code: "no-sdk-clients", - target: getNoSdkClientsDiagnosticTarget(program), + target: listServices(program)[0]?.type ?? NoTarget, }); } From eaa15e8505ecabd77353ed089ba0f96edf412ece Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 12:01:20 -0400 Subject: [PATCH 04/11] fix(emitter): remove unused program import Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/http-client-python/emitter/src/emitter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 2057fe3106e..f0603ba78a3 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,5 +1,5 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; -import type { EmitContext, Program } from "@typespec/compiler"; +import type { EmitContext } from "@typespec/compiler"; import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; From 4fed492ad4a2c7b3252a6a6a8ba8fb88e4af3050 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 12:07:39 -0400 Subject: [PATCH 05/11] fix(emitter): address diagnostic review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../http-client-python/emitter/src/emitter.ts | 4 +- .../emitter/test/emitter.test.ts | 44 ++++++++----------- .../emitter/test/test-host.ts | 6 +++ 3 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 packages/http-client-python/emitter/test/test-host.ts diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index f0603ba78a3..2e4347b8c73 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -181,11 +181,11 @@ async function onEmitMain(context: EmitContext) { const yamlMap = emitCodeModel(sdkContext); const parsedYamlMap = walkThroughNodes(yamlMap); - // Python emitter requires an SDK client in the TypeSpec + // Warn when no SDK clients are present, while still allowing model-only generation. if (sdkContext.sdkPackage.clients.length === 0) { reportDiagnostic(program, { code: "no-sdk-clients", - target: listServices(program)[0]?.type ?? NoTarget, + target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), }); } diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index 16f63c7ecd5..bef6bedea18 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -1,37 +1,31 @@ -import { NoTarget, resolvePath } from "@typespec/compiler"; -import { createTester, t } from "@typespec/compiler/testing"; -import { strictEqual } from "assert"; +import { listServices } from "@typespec/compiler"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { it } from "vitest"; -import { getNoSdkClientsDiagnosticTarget } from "../src/emitter.js"; import { reportDiagnostic } from "../src/lib.js"; - -const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); +import { Tester } from "./test-host.js"; it("targets the service namespace when no SDK clients are found", async () => { - const { Service, program } = await Tester.compile(t.code` + const { program } = await Tester.compile(t.code` #suppress "@typespec/http-client-python/no-sdk-clients" "This service intentionally has no client." @service namespace ${t.namespace("Service")} {} `); - const target = getNoSdkClientsDiagnosticTarget(program); - strictEqual(target, Service); - - reportDiagnostic(program, { code: "no-sdk-clients", target }); - strictEqual( - program.diagnostics.some((x) => x.code.endsWith("/no-sdk-clients")), - false, - ); + reportDiagnostic(program, { + code: "no-sdk-clients", + target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), + }); + expectDiagnostics(program.diagnostics, []); }); -it("uses no target when no service exists", async () => { - const { program } = await Tester.compile(`model Widget {}`); - - const target = getNoSdkClientsDiagnosticTarget(program); - strictEqual(target, NoTarget); +it("allows suppressing the warning when no service exists", async () => { + const { program } = await Tester.compile(` + #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." + model Widget {} + `); - reportDiagnostic(program, { code: "no-sdk-clients", target }); - strictEqual( - program.diagnostics.some((x) => x.code.endsWith("/no-sdk-clients")), - true, - ); + reportDiagnostic(program, { + code: "no-sdk-clients", + target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), + }); + expectDiagnostics(program.diagnostics, []); }); diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts new file mode 100644 index 00000000000..bb93ccc2bec --- /dev/null +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -0,0 +1,6 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; + +const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); + +export const Tester = PythonTester; From f6cedca4db974bb378fefb7b4c0b47dcf5995c54 Mon Sep 17 00:00:00 2001 From: iscai-msft <43154838+iscai-msft@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:08:30 -0400 Subject: [PATCH 06/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/http-client-python/emitter/src/lib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index 740e79be11f..0ee8f1bf41f 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -183,7 +183,7 @@ const libDef = { severity: "warning", messages: { default: - "The Python emitter did not find any SDK clients in this TypeSpec program. The current Python generator expects at least one client/service to generate code.", + "The Python emitter did not find any SDK clients in this TypeSpec program. No client code will be generated (models can still be emitted). Suppress this warning if this is expected.", }, }, "browser-runtime-load-failed": { From e8db65040ae4b1b75ce3b1040d77afe34ae05331 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 12:11:58 -0400 Subject: [PATCH 07/11] test(emitter): cover model-only generation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../emitter/test/emitter.test.ts | 33 +++++++++---------- .../emitter/test/test-host.ts | 4 +++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index bef6bedea18..11c6e4ac35f 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -1,31 +1,28 @@ -import { listServices } from "@typespec/compiler"; import { expectDiagnostics, t } from "@typespec/compiler/testing"; -import { it } from "vitest"; -import { reportDiagnostic } from "../src/lib.js"; -import { Tester } from "./test-host.js"; +import { expect } from "vitest"; +import { EmitterTester } from "./test-host.js"; it("targets the service namespace when no SDK clients are found", async () => { - const { program } = await Tester.compile(t.code` + const [, diagnostics] = await EmitterTester.compileAndDiagnose(t.code` #suppress "@typespec/http-client-python/no-sdk-clients" "This service intentionally has no client." @service namespace ${t.namespace("Service")} {} `); - reportDiagnostic(program, { - code: "no-sdk-clients", - target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), - }); - expectDiagnostics(program.diagnostics, []); + expectDiagnostics(diagnostics, []); }); -it("allows suppressing the warning when no service exists", async () => { - const { program } = await Tester.compile(` +it("generates models when no service exists", async () => { + const [result, diagnostics] = await EmitterTester.compileAndDiagnose(` #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." - model Widget {} + namespace Models { + model Widget {} + } `); - reportDiagnostic(program, { - code: "no-sdk-clients", - target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), - }); - expectDiagnostics(program.diagnostics, []); + expectDiagnostics(diagnostics, []); + expect( + Object.entries(result.outputs).some( + ([path, content]) => path.endsWith("models/_models.py") && content.includes("class Widget"), + ), + ).toBe(true); }); diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts index bb93ccc2bec..1036d88b52f 100644 --- a/packages/http-client-python/emitter/test/test-host.ts +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -4,3 +4,7 @@ import { createTester } from "@typespec/compiler/testing"; const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); export const Tester = PythonTester; +export const EmitterTester = PythonTester.emit("@typespec/http-client-python", { + "generate-packaging-files": false, + "use-pyodide": true, +}); From 09c7c725d190ab6d51cb7cac882e0aa4f483eb7f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 12:43:46 -0400 Subject: [PATCH 08/11] fix(python): test model-only generation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9baae876-cdbf-4556-952a-e95b26298ca9 --- .../http-client-python/emitter/src/emitter.ts | 42 ++++++++---- .../emitter/test/emitter.test.ts | 66 ++++++++++++++----- .../emitter/test/test-host.ts | 18 ++++- 3 files changed, 95 insertions(+), 31 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 2e4347b8c73..cbd2d788287 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,6 +1,11 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; import type { EmitContext } from "@typespec/compiler"; -import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; +import { + emitFile, + joinPaths, + listServices, + NoTarget, +} from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; import { @@ -92,7 +97,10 @@ function walkThroughNodes(yamlMap: Record): Record { } } else if (Array.isArray(current[key])) { stack.push(current[key]); - } else if (current[key] !== undefined && typeof current[key] === "object") { + } else if ( + current[key] !== undefined && + typeof current[key] === "object" + ) { stack.push(current[key]); } } @@ -164,7 +172,9 @@ export async function $onEmit(context: EmitContext) { "========================================= error stack start ================================================"; const errStackEnd = "========================================= error stack end ================================================"; - const errStack = error.stack ? `\n${errStackStart}\n${error.stack}\n${errStackEnd}` : ""; + const errStack = error.stack + ? `\n${errStackStart}\n${error.stack}\n${errStackEnd}` + : ""; reportDiagnostic(context.program, { code: "unknown-error", target: NoTarget, @@ -185,24 +195,29 @@ async function onEmitMain(context: EmitContext) { if (sdkContext.sdkPackage.clients.length === 0) { reportDiagnostic(program, { code: "no-sdk-clients", - target: listServices(program)[0]?.type ?? program.getGlobalNamespaceType(), + target: + listServices(program)[0]?.type ?? + program.getGlobalNamespaceType().models.values().next().value ?? + program.getGlobalNamespaceType(), }); } const resolvedOptions = sdkContext.emitContext.options; const commandArgs: Record = {}; if (resolvedOptions["packaging-files-config"]) { - const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map( - ([key, value]) => { - return `${key}:${value}`; - }, - ); + const keyValuePairs = Object.entries( + resolvedOptions["packaging-files-config"], + ).map(([key, value]) => { + return `${key}:${value}`; + }); commandArgs["packaging-files-config"] = keyValuePairs.join("|"); resolvedOptions["packaging-files-config"] = undefined; } if (resolvedOptions["keep-pyproject-fields"]) { // Flatten the object of enabled fields into a comma-separated list for the generator. - const enabledFields = Object.entries(resolvedOptions["keep-pyproject-fields"]) + const enabledFields = Object.entries( + resolvedOptions["keep-pyproject-fields"], + ) .filter(([, value]) => value === true) .map(([key]) => key); commandArgs["keep-pyproject-fields"] = enabledFields.join(","); @@ -214,8 +229,11 @@ async function onEmitMain(context: EmitContext) { commandArgs[key] = value; } if (resolvedOptions["generate-packaging-files"]) { - commandArgs["package-mode"] = sdkContext.arm ? "azure-mgmt" : "azure-dataplane"; - commandArgs["keep-setup-py"] = resolvedOptions["keep-setup-py"] === true ? "true" : "false"; + commandArgs["package-mode"] = sdkContext.arm + ? "azure-mgmt" + : "azure-dataplane"; + commandArgs["keep-setup-py"] = + resolvedOptions["keep-setup-py"] === true ? "true" : "false"; } if (sdkContext.arm === true) { commandArgs["azure-arm"] = "true"; diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index 11c6e4ac35f..062b4b70fae 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -1,28 +1,62 @@ import { expectDiagnostics, t } from "@typespec/compiler/testing"; -import { expect } from "vitest"; +import { mkdtemp, readFile, rm } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; +import { expect, it } from "vitest"; import { EmitterTester } from "./test-host.js"; it("targets the service namespace when no SDK clients are found", async () => { - const [, diagnostics] = await EmitterTester.compileAndDiagnose(t.code` + const [, diagnostics] = await EmitterTester.compileAndDiagnose( + t.code` #suppress "@typespec/http-client-python/no-sdk-clients" "This service intentionally has no client." @service namespace ${t.namespace("Service")} {} - `); + `, + { + compilerOptions: { + options: { + "@typespec/http-client-python": { + "emit-yaml-only": true, + }, + }, + }, + }, + ); expectDiagnostics(diagnostics, []); }); it("generates models when no service exists", async () => { - const [result, diagnostics] = await EmitterTester.compileAndDiagnose(` - #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." - namespace Models { - model Widget {} - } - `); + const outputDir = await mkdtemp(join(tmpdir(), "typespec-python-models-")); + try { + const [, diagnostics] = await EmitterTester.compileAndDiagnose( + ` + import "@azure-tools/typespec-client-generator-core"; + using Azure.ClientGenerator.Core; - expectDiagnostics(diagnostics, []); - expect( - Object.entries(result.outputs).some( - ([path, content]) => path.endsWith("models/_models.py") && content.includes("class Widget"), - ), - ).toBe(true); -}); + #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." + @access(Access.public) + @usage(Usage.input | Usage.output) + @clientNamespace("Models") + model Widget {} + `, + { + compilerOptions: { + options: { + "@typespec/http-client-python": { + "emitter-output-dir": outputDir, + }, + }, + }, + }, + ); + + expectDiagnostics(diagnostics, []); + const model = await readFile( + join(outputDir, "models", "models", "_models.py"), + "utf-8", + ); + expect(model).toContain("class Widget"); + } finally { + await rm(outputDir, { recursive: true, force: true }); + } +}, 30_000); diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts index 1036d88b52f..f91d3ba71f5 100644 --- a/packages/http-client-python/emitter/test/test-host.ts +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -1,10 +1,22 @@ import { resolvePath } from "@typespec/compiler"; -import { createTester } from "@typespec/compiler/testing"; +import { createTester, mockFile } from "@typespec/compiler/testing"; +import { $onEmit } from "../src/emitter.js"; -const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] }); +const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { + libraries: ["@azure-tools/typespec-client-generator-core"], +}); export const Tester = PythonTester; -export const EmitterTester = PythonTester.emit("@typespec/http-client-python", { +export const EmitterTester = PythonTester.files({ + "node_modules/@typespec/http-client-python/package.json": JSON.stringify({ + name: "@typespec/http-client-python", + version: "0.0.0", + exports: { ".": "./index.js" }, + }), + "node_modules/@typespec/http-client-python/index.js": mockFile.js({ + $onEmit, + }), +}).emit("@typespec/http-client-python", { "generate-packaging-files": false, "use-pyodide": true, }); From f49c7fca3ad9af0af9656967b689d752794e46b8 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 12:53:49 -0400 Subject: [PATCH 09/11] test(python): assert code model instead of running generator Drive the model-only test off the emitter's code model output (emit-yaml-only) rather than booting the Python/Pyodide generator, so it runs in ~400ms without a long timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9baae876-cdbf-4556-952a-e95b26298ca9 --- .../emitter/test/emitter.test.ts | 70 ++++++------------- .../emitter/test/test-host.ts | 63 ++++++++++++++++- 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index 062b4b70fae..787b80069f5 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -1,62 +1,34 @@ import { expectDiagnostics, t } from "@typespec/compiler/testing"; -import { mkdtemp, readFile, rm } from "fs/promises"; -import { tmpdir } from "os"; -import { join } from "path"; import { expect, it } from "vitest"; -import { EmitterTester } from "./test-host.js"; +import { emitCodeModel } from "./test-host.js"; it("targets the service namespace when no SDK clients are found", async () => { - const [, diagnostics] = await EmitterTester.compileAndDiagnose( - t.code` + const { diagnostics } = await emitCodeModel(t.code` #suppress "@typespec/http-client-python/no-sdk-clients" "This service intentionally has no client." @service namespace ${t.namespace("Service")} {} - `, - { - compilerOptions: { - options: { - "@typespec/http-client-python": { - "emit-yaml-only": true, - }, - }, - }, - }, - ); + `); expectDiagnostics(diagnostics, []); }); it("generates models when no service exists", async () => { - const outputDir = await mkdtemp(join(tmpdir(), "typespec-python-models-")); - try { - const [, diagnostics] = await EmitterTester.compileAndDiagnose( - ` - import "@azure-tools/typespec-client-generator-core"; - using Azure.ClientGenerator.Core; + const { codeModel, diagnostics } = await emitCodeModel(` + import "@azure-tools/typespec-client-generator-core"; + using Azure.ClientGenerator.Core; - #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." - @access(Access.public) - @usage(Usage.input | Usage.output) - @clientNamespace("Models") - model Widget {} - `, - { - compilerOptions: { - options: { - "@typespec/http-client-python": { - "emitter-output-dir": outputDir, - }, - }, - }, - }, - ); + #suppress "@typespec/http-client-python/no-sdk-clients" "This model-only package intentionally has no client." + @access(Access.public) + @usage(Usage.input | Usage.output) + @clientNamespace("Models") + model Widget {} + `); - expectDiagnostics(diagnostics, []); - const model = await readFile( - join(outputDir, "models", "models", "_models.py"), - "utf-8", - ); - expect(model).toContain("class Widget"); - } finally { - await rm(outputDir, { recursive: true, force: true }); - } -}, 30_000); + expectDiagnostics(diagnostics, []); + // A model-only package has no clients but must still emit its models into the code model. + expect(codeModel.clients).toHaveLength(0); + expect( + codeModel.types.some( + (type) => type.type === "model" && type.name === "Widget", + ), + ).toBe(true); +}); diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts index f91d3ba71f5..42a543d4250 100644 --- a/packages/http-client-python/emitter/test/test-host.ts +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -1,5 +1,10 @@ +import type { Diagnostic } from "@typespec/compiler"; import { resolvePath } from "@typespec/compiler"; import { createTester, mockFile } from "@typespec/compiler/testing"; +import { mkdtemp, readdir, readFile, rm } from "fs/promises"; +import jsyaml from "js-yaml"; +import { tmpdir } from "os"; +import { join } from "path"; import { $onEmit } from "../src/emitter.js"; const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { @@ -7,6 +12,10 @@ const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), { }); export const Tester = PythonTester; + +// `@typespec/http-client-python` is intentionally excluded from the pnpm workspace, so it isn't +// linked into its own `node_modules` and `.emit()` can't resolve it by name. Register a minimal +// virtual package whose entrypoint re-exports the real `$onEmit`, so the emitter under test runs. export const EmitterTester = PythonTester.files({ "node_modules/@typespec/http-client-python/package.json": JSON.stringify({ name: "@typespec/http-client-python", @@ -18,5 +27,57 @@ export const EmitterTester = PythonTester.files({ }), }).emit("@typespec/http-client-python", { "generate-packaging-files": false, - "use-pyodide": true, }); + +/** A type entry in the emitted code model. */ +export interface CodeModelType { + type: string; + name?: string; +} + +/** The subset of the emitted code model that the emitter tests assert against. */ +export interface CodeModel { + clients: unknown[]; + types: CodeModelType[]; +} + +/** + * Run the emitter's TypeScript step only (via `emit-yaml-only`) and return the code model it + * produced along with any diagnostics. This stops right after `emitCodeModel`, so it exercises the + * real emitter logic that decides what gets generated without booting the Python/Pyodide generator. + */ +export async function emitCodeModel( + code: string, +): Promise<{ codeModel: CodeModel; diagnostics: readonly Diagnostic[] }> { + const outputDir = await mkdtemp(join(tmpdir(), "typespec-python-")); + try { + const [, diagnostics] = await EmitterTester.compileAndDiagnose(code, { + compilerOptions: { + options: { + "@typespec/http-client-python": { + "emit-yaml-only": true, + "emitter-output-dir": outputDir, + }, + }, + }, + }); + + // `emit-yaml-only` writes a `.tsp-codegen-*.json` pointer into the output dir that references + // the serialized code model YAML written to the OS temp dir. + const pointerName = (await readdir(outputDir)).find( + (name) => name.startsWith(".tsp-codegen-") && name.endsWith(".json"), + ); + if (!pointerName) { + throw new Error("Emitter did not produce a code model."); + } + const { yamlPath } = JSON.parse( + await readFile(join(outputDir, pointerName), "utf-8"), + ); + const codeModel = jsyaml.load( + await readFile(yamlPath, "utf-8"), + ) as CodeModel; + return { codeModel, diagnostics }; + } finally { + await rm(outputDir, { recursive: true, force: true }); + } +} From 80e05f09ce9ff36c340d37b9f50ec98d2eb286d4 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 2 Sep 2026 19:44:46 -0400 Subject: [PATCH 10/11] fix(python): target first SDK model for no-sdk-clients diagnostic Point the no-sdk-clients diagnostic at the first SDK model's raw type when there is no service, so it stays suppressable for model-only packages whose models live under an explicit namespace (the global namespace has no suppressable node). Also delete the code-model YAML temp file the emit-yaml-only test helper produces. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9baae876-cdbf-4556-952a-e95b26298ca9 --- packages/http-client-python/emitter/src/emitter.ts | 2 +- packages/http-client-python/emitter/test/test-host.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index cbd2d788287..d54bdc9157a 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -197,7 +197,7 @@ async function onEmitMain(context: EmitContext) { code: "no-sdk-clients", target: listServices(program)[0]?.type ?? - program.getGlobalNamespaceType().models.values().next().value ?? + sdkContext.sdkPackage.models[0]?.__raw ?? program.getGlobalNamespaceType(), }); } diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts index 42a543d4250..d0a93785b3d 100644 --- a/packages/http-client-python/emitter/test/test-host.ts +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -50,6 +50,7 @@ export async function emitCodeModel( code: string, ): Promise<{ codeModel: CodeModel; diagnostics: readonly Diagnostic[] }> { const outputDir = await mkdtemp(join(tmpdir(), "typespec-python-")); + let yamlPath: string | undefined; try { const [, diagnostics] = await EmitterTester.compileAndDiagnose(code, { compilerOptions: { @@ -70,14 +71,17 @@ export async function emitCodeModel( if (!pointerName) { throw new Error("Emitter did not produce a code model."); } - const { yamlPath } = JSON.parse( + ({ yamlPath } = JSON.parse( await readFile(join(outputDir, pointerName), "utf-8"), - ); + )); const codeModel = jsyaml.load( - await readFile(yamlPath, "utf-8"), + await readFile(yamlPath!, "utf-8"), ) as CodeModel; return { codeModel, diagnostics }; } finally { await rm(outputDir, { recursive: true, force: true }); + if (yamlPath) { + await rm(yamlPath, { force: true }); + } } } From 516407f9bae45dd0d9beb9cf0004d3e23406368a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:24:26 +0000 Subject: [PATCH 11/11] chore(python): format emitter changes Co-authored-by: msyyc <70930885+msyyc@users.noreply.github.com> --- .../http-client-python/emitter/src/emitter.ts | 37 ++++++------------- .../emitter/test/emitter.test.ts | 8 ++-- .../emitter/test/test-host.ts | 8 +--- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index d54bdc9157a..c68517f99f4 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -1,11 +1,6 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; import type { EmitContext } from "@typespec/compiler"; -import { - emitFile, - joinPaths, - listServices, - NoTarget, -} from "@typespec/compiler"; +import { emitFile, joinPaths, listServices, NoTarget } from "@typespec/compiler"; import pkgJson from "../../package.json" with { type: "json" }; import { emitCodeModel } from "./code-model.js"; import { @@ -97,10 +92,7 @@ function walkThroughNodes(yamlMap: Record): Record { } } else if (Array.isArray(current[key])) { stack.push(current[key]); - } else if ( - current[key] !== undefined && - typeof current[key] === "object" - ) { + } else if (current[key] !== undefined && typeof current[key] === "object") { stack.push(current[key]); } } @@ -172,9 +164,7 @@ export async function $onEmit(context: EmitContext) { "========================================= error stack start ================================================"; const errStackEnd = "========================================= error stack end ================================================"; - const errStack = error.stack - ? `\n${errStackStart}\n${error.stack}\n${errStackEnd}` - : ""; + const errStack = error.stack ? `\n${errStackStart}\n${error.stack}\n${errStackEnd}` : ""; reportDiagnostic(context.program, { code: "unknown-error", target: NoTarget, @@ -205,19 +195,17 @@ async function onEmitMain(context: EmitContext) { const resolvedOptions = sdkContext.emitContext.options; const commandArgs: Record = {}; if (resolvedOptions["packaging-files-config"]) { - const keyValuePairs = Object.entries( - resolvedOptions["packaging-files-config"], - ).map(([key, value]) => { - return `${key}:${value}`; - }); + const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map( + ([key, value]) => { + return `${key}:${value}`; + }, + ); commandArgs["packaging-files-config"] = keyValuePairs.join("|"); resolvedOptions["packaging-files-config"] = undefined; } if (resolvedOptions["keep-pyproject-fields"]) { // Flatten the object of enabled fields into a comma-separated list for the generator. - const enabledFields = Object.entries( - resolvedOptions["keep-pyproject-fields"], - ) + const enabledFields = Object.entries(resolvedOptions["keep-pyproject-fields"]) .filter(([, value]) => value === true) .map(([key]) => key); commandArgs["keep-pyproject-fields"] = enabledFields.join(","); @@ -229,11 +217,8 @@ async function onEmitMain(context: EmitContext) { commandArgs[key] = value; } if (resolvedOptions["generate-packaging-files"]) { - commandArgs["package-mode"] = sdkContext.arm - ? "azure-mgmt" - : "azure-dataplane"; - commandArgs["keep-setup-py"] = - resolvedOptions["keep-setup-py"] === true ? "true" : "false"; + commandArgs["package-mode"] = sdkContext.arm ? "azure-mgmt" : "azure-dataplane"; + commandArgs["keep-setup-py"] = resolvedOptions["keep-setup-py"] === true ? "true" : "false"; } if (sdkContext.arm === true) { commandArgs["azure-arm"] = "true"; diff --git a/packages/http-client-python/emitter/test/emitter.test.ts b/packages/http-client-python/emitter/test/emitter.test.ts index 787b80069f5..ecb2081d5a1 100644 --- a/packages/http-client-python/emitter/test/emitter.test.ts +++ b/packages/http-client-python/emitter/test/emitter.test.ts @@ -26,9 +26,7 @@ it("generates models when no service exists", async () => { expectDiagnostics(diagnostics, []); // A model-only package has no clients but must still emit its models into the code model. expect(codeModel.clients).toHaveLength(0); - expect( - codeModel.types.some( - (type) => type.type === "model" && type.name === "Widget", - ), - ).toBe(true); + expect(codeModel.types.some((type) => type.type === "model" && type.name === "Widget")).toBe( + true, + ); }); diff --git a/packages/http-client-python/emitter/test/test-host.ts b/packages/http-client-python/emitter/test/test-host.ts index d0a93785b3d..07a762f578d 100644 --- a/packages/http-client-python/emitter/test/test-host.ts +++ b/packages/http-client-python/emitter/test/test-host.ts @@ -71,12 +71,8 @@ export async function emitCodeModel( if (!pointerName) { throw new Error("Emitter did not produce a code model."); } - ({ yamlPath } = JSON.parse( - await readFile(join(outputDir, pointerName), "utf-8"), - )); - const codeModel = jsyaml.load( - await readFile(yamlPath!, "utf-8"), - ) as CodeModel; + ({ yamlPath } = JSON.parse(await readFile(join(outputDir, pointerName), "utf-8"))); + const codeModel = jsyaml.load(await readFile(yamlPath!, "utf-8")) as CodeModel; return { codeModel, diagnostics }; } finally { await rm(outputDir, { recursive: true, force: true });