Skip to content
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 31 additions & 14 deletions packages/http-client-python/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
import type { EmitContext } from "@typespec/compiler";
import { emitFile, joinPaths, 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 {
Expand Down Expand Up @@ -92,7 +97,10 @@ function walkThroughNodes(yamlMap: Record<string, any>): Record<string, any> {
}
} 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]);
}
}
Expand Down Expand Up @@ -164,7 +172,9 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
"========================================= 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,
Expand All @@ -181,29 +191,33 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
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) {
Comment thread
msyyc marked this conversation as resolved.
reportDiagnostic(program, {
code: "no-sdk-clients",
target: NoTarget,
target:
listServices(program)[0]?.type ??
program.getGlobalNamespaceType().models.values().next().value ??
program.getGlobalNamespaceType(),
});
Comment thread
iscai-msft marked this conversation as resolved.
Comment thread
iscai-msft marked this conversation as resolved.
return;
}
Comment thread
msyyc marked this conversation as resolved.
Outdated

const resolvedOptions = sdkContext.emitContext.options;
const commandArgs: Record<string, string> = {};
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(",");
Expand All @@ -215,8 +229,11 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
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";
Expand Down
4 changes: 2 additions & 2 deletions packages/http-client-python/emitter/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ 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.",
"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": {
Expand Down
62 changes: 62 additions & 0 deletions packages/http-client-python/emitter/test/emitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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";

it("targets the service namespace when no SDK clients are found", async () => {
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 outputDir = await mkdtemp(join(tmpdir(), "typespec-python-models-"));
try {
Comment thread
iscai-msft marked this conversation as resolved.
Outdated
const [, diagnostics] = await EmitterTester.compileAndDiagnose(
Comment thread
iscai-msft marked this conversation as resolved.
Outdated
`
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: {
Comment thread
iscai-msft marked this conversation as resolved.
Outdated
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);
22 changes: 22 additions & 0 deletions packages/http-client-python/emitter/test/test-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { resolvePath } from "@typespec/compiler";
import { createTester, mockFile } from "@typespec/compiler/testing";
import { $onEmit } from "../src/emitter.js";

const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), {
libraries: ["@azure-tools/typespec-client-generator-core"],
});

export const Tester = PythonTester;
export const EmitterTester = PythonTester.files({
"node_modules/@typespec/http-client-python/package.json": JSON.stringify({
Comment thread
iscai-msft marked this conversation as resolved.
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,
});
Loading