@typespec/http-client-python 0.6.11 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/emitter/emitter.d.ts.map +1 -1
  2. package/dist/emitter/emitter.js +114 -51
  3. package/dist/emitter/emitter.js.map +1 -1
  4. package/dist/emitter/lib.d.ts +28 -1
  5. package/dist/emitter/lib.d.ts.map +1 -1
  6. package/dist/emitter/lib.js +21 -1
  7. package/dist/emitter/lib.js.map +1 -1
  8. package/dist/emitter/types.d.ts.map +1 -1
  9. package/dist/emitter/types.js +3 -1
  10. package/dist/emitter/types.js.map +1 -1
  11. package/dist/emitter/utils.d.ts +1 -0
  12. package/dist/emitter/utils.d.ts.map +1 -1
  13. package/dist/emitter/utils.js +78 -0
  14. package/dist/emitter/utils.js.map +1 -1
  15. package/emitter/src/emitter.ts +119 -51
  16. package/emitter/src/lib.ts +22 -1
  17. package/emitter/src/types.ts +4 -1
  18. package/emitter/src/utils.ts +82 -0
  19. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  20. package/emitter/test/utils.test.ts +6 -1
  21. package/eng/scripts/Build-Packages.ps1 +3 -2
  22. package/eng/scripts/ci/regenerate.ts +31 -4
  23. package/eng/scripts/ci/run_apiview.py +5 -0
  24. package/eng/scripts/setup/__pycache__/venvtools.cpython-38.pyc +0 -0
  25. package/eng/scripts/setup/run_tsp.py +2 -3
  26. package/generator/build/lib/pygen/codegen/templates/model_dpg.py.jinja2 +0 -10
  27. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +1 -1
  28. package/generator/component-detection-pip-report.json +2 -2
  29. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  30. package/generator/pygen/codegen/templates/model_dpg.py.jinja2 +0 -10
  31. package/generator/pygen/codegen/templates/serialization.py.jinja2 +1 -1
  32. package/generator/pygen.egg-info/PKG-INFO +0 -1
  33. package/generator/pygen.egg-info/SOURCES.txt +0 -1
  34. package/generator/pygen.egg-info/requires.txt +0 -1
  35. package/generator/setup.py +0 -1
  36. package/package.json +24 -23
  37. package/generator/build/lib/pygen/m2r.py +0 -65
  38. package/generator/pygen/m2r.py +0 -65
  39. package/generator/test/generic_mock_api_tests/unittests/test_m2r.py +0 -10
@@ -8,13 +8,15 @@ import { EmitContext, NoTarget } from "@typespec/compiler";
8
8
  import { execSync } from "child_process";
9
9
  import fs from "fs";
10
10
  import path, { dirname } from "path";
11
+ import process from "process";
11
12
  import { loadPyodide } from "pyodide";
12
13
  import { fileURLToPath } from "url";
13
14
  import { emitCodeModel } from "./code-model.js";
14
15
  import { saveCodeModelAsYaml } from "./external-process.js";
15
16
  import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js";
16
17
  import { runPython3 } from "./run-python3.js";
17
- import { removeUnderscoresFromNamespace } from "./utils.js";
18
+ import { disableGenerationMap, simpleTypesMap, typesMap } from "./types.js";
19
+ import { md2Rst, removeUnderscoresFromNamespace } from "./utils.js";
18
20
 
19
21
  export function getModelsMode(context: SdkContext): "dpg" | "none" {
20
22
  const specifiedModelsMode = context.emitContext.options["models-mode"];
@@ -23,9 +25,11 @@ export function getModelsMode(context: SdkContext): "dpg" | "none" {
23
25
  if (modelModes.includes(specifiedModelsMode)) {
24
26
  return specifiedModelsMode;
25
27
  }
26
- throw new Error(
27
- `Need to specify models mode with the following values: ${modelModes.join(", ")}`,
28
- );
28
+ reportDiagnostic(context.program, {
29
+ code: "invalid-models-mode",
30
+ target: NoTarget,
31
+ format: { inValidValue: specifiedModelsMode },
32
+ });
29
33
  }
30
34
  return "dpg";
31
35
  }
@@ -76,7 +80,54 @@ async function createPythonSdkContext<TServiceOperation extends SdkServiceOperat
76
80
  };
77
81
  }
78
82
 
83
+ function walkThroughNodes(yamlMap: Record<string, any>): Record<string, any> {
84
+ const stack = [yamlMap];
85
+ const seen = new WeakSet();
86
+
87
+ while (stack.length > 0) {
88
+ const current = stack.pop();
89
+
90
+ if (seen.has(current!)) {
91
+ continue;
92
+ }
93
+ if (current !== undefined && current !== null) {
94
+ seen.add(current);
95
+ }
96
+
97
+ if (Array.isArray(current)) {
98
+ for (let i = 0; i < current.length; i++) {
99
+ if (current[i] !== undefined && typeof current[i] === "object") {
100
+ stack.push(current[i]);
101
+ }
102
+ }
103
+ } else {
104
+ for (const key in current) {
105
+ if (key === "description" || key === "summary") {
106
+ if (current[key] !== undefined) {
107
+ current[key] = md2Rst(current[key]);
108
+ }
109
+ } else if (Array.isArray(current[key])) {
110
+ stack.push(current[key]);
111
+ } else if (current[key] !== undefined && typeof current[key] === "object") {
112
+ stack.push(current[key]);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ return yamlMap;
119
+ }
120
+
121
+ function cleanAllCache() {
122
+ typesMap.clear();
123
+ simpleTypesMap.clear();
124
+ disableGenerationMap.clear();
125
+ }
126
+
79
127
  export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
128
+ // clean all cache to make sure emitter could work in watch mode
129
+ cleanAllCache();
130
+
80
131
  const program = context.program;
81
132
  const sdkContext = await createPythonSdkContext<SdkHttpOperation>(context);
82
133
  const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -90,7 +141,10 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
90
141
  });
91
142
  return;
92
143
  }
93
- const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap);
144
+
145
+ const parsedYamlMap = walkThroughNodes(yamlMap);
146
+
147
+ const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap);
94
148
  const resolvedOptions = sdkContext.emitContext.options;
95
149
  const commandArgs: Record<string, string> = {};
96
150
  if (resolvedOptions["packaging-files-config"]) {
@@ -134,54 +188,68 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
134
188
  }
135
189
  }
136
190
 
137
- if (resolvedOptions["use-pyodide"]) {
138
- // here we run with pyodide
139
- const pyodide = await setupPyodideCall(root);
140
- // create the output folder if not exists
141
- if (!fs.existsSync(outputDir)) {
142
- fs.mkdirSync(outputDir, { recursive: true });
143
- }
144
- // mount output folder to pyodide
145
- pyodide.FS.mkdirTree("/output");
146
- pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output");
147
- // mount yaml file to pyodide
148
- pyodide.FS.mkdirTree("/yaml");
149
- pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml");
150
- const globals = pyodide.toPy({
151
- outputFolder: "/output",
152
- yamlFile: `/yaml/${path.basename(yamlPath)}`,
153
- commandArgs,
154
- });
155
- const pythonCode = `
156
- async def main():
157
- import warnings
158
- with warnings.catch_warnings():
159
- warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues
160
- from pygen import m2r, preprocess, codegen, black
161
- m2r.M2R(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process()
162
- preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process()
163
- codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process()
164
- black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process()
165
-
166
- await main()`;
167
- await pyodide.runPythonAsync(pythonCode, { globals });
168
- } else {
169
- // here we run with native python
170
- let venvPath = path.join(root, "venv");
171
- if (fs.existsSync(path.join(venvPath, "bin"))) {
172
- venvPath = path.join(venvPath, "bin", "python");
173
- } else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
174
- venvPath = path.join(venvPath, "Scripts", "python.exe");
191
+ try {
192
+ if (resolvedOptions["use-pyodide"]) {
193
+ // here we run with pyodide
194
+ const pyodide = await setupPyodideCall(root);
195
+ // create the output folder if not exists
196
+ if (!fs.existsSync(outputDir)) {
197
+ fs.mkdirSync(outputDir, { recursive: true });
198
+ }
199
+ // mount output folder to pyodide
200
+ pyodide.FS.mkdirTree("/output");
201
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output");
202
+ // mount yaml file to pyodide
203
+ pyodide.FS.mkdirTree("/yaml");
204
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml");
205
+ const globals = pyodide.toPy({
206
+ outputFolder: "/output",
207
+ yamlFile: `/yaml/${path.basename(yamlPath)}`,
208
+ commandArgs,
209
+ });
210
+ const pythonCode = `
211
+ async def main():
212
+ import warnings
213
+ with warnings.catch_warnings():
214
+ from pygen import preprocess, codegen, black
215
+ preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process()
216
+ codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process()
217
+ black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process()
218
+
219
+ await main()`;
220
+ await pyodide.runPythonAsync(pythonCode, { globals });
175
221
  } else {
176
- throw new Error("Virtual environment doesn't exist.");
222
+ // here we run with native python
223
+ let venvPath = path.join(root, "venv");
224
+ if (fs.existsSync(path.join(venvPath, "bin"))) {
225
+ venvPath = path.join(venvPath, "bin", "python");
226
+ } else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
227
+ venvPath = path.join(venvPath, "Scripts", "python.exe");
228
+ } else {
229
+ reportDiagnostic(program, {
230
+ code: "pyodide-flag-conflict",
231
+ target: NoTarget,
232
+ });
233
+ }
234
+ commandArgs["output-folder"] = outputDir;
235
+ commandArgs["cadl-file"] = yamlPath;
236
+ const commandFlags = Object.entries(commandArgs)
237
+ .map(([key, value]) => `--${key}=${value}`)
238
+ .join(" ");
239
+ const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`;
240
+ execSync(command, { stdio: [process.stdin, process.stdout] });
177
241
  }
178
- commandArgs["output-folder"] = outputDir;
179
- commandArgs["cadl-file"] = yamlPath;
180
- const commandFlags = Object.entries(commandArgs)
181
- .map(([key, value]) => `--${key}=${value}`)
182
- .join(" ");
183
- const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`;
184
- execSync(command);
242
+ } catch (error: any) {
243
+ const errStackStart =
244
+ "========================================= error stack start ================================================";
245
+ const errStackEnd =
246
+ "========================================= error stack end ================================================";
247
+ const errStack = error.stack ? `\n${errStackStart}\n${error.stack}\n${errStackEnd}` : "";
248
+ reportDiagnostic(program, {
249
+ code: "unknown-error",
250
+ target: NoTarget,
251
+ format: { stack: errStack },
252
+ });
185
253
  }
186
254
  }
187
255
  }
@@ -1,5 +1,5 @@
1
1
  import { SdkContext, SdkServiceOperation } from "@azure-tools/typespec-client-generator-core";
2
- import { createTypeSpecLibrary, JSONSchemaType } from "@typespec/compiler";
2
+ import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
3
3
 
4
4
  export interface PythonEmitterOptions {
5
5
  "package-version"?: string;
@@ -56,6 +56,27 @@ const EmitterOptionsSchema: JSONSchemaType<PythonEmitterOptions> = {
56
56
  const libDef = {
57
57
  name: "@typespec/http-client-python",
58
58
  diagnostics: {
59
+ // error
60
+ "unknown-error": {
61
+ severity: "error",
62
+ messages: {
63
+ default: paramMessage`Can't generate Python client code from this TypeSpec. Please open an issue on https://github.com/microsoft/typespec'.${"stack"}`,
64
+ },
65
+ },
66
+ "invalid-models-mode": {
67
+ severity: "error",
68
+ messages: {
69
+ default: paramMessage`Invalid value '${"inValidValue"}' for 'models-mode' of tspconfig.yaml and expected values are 'dpg'/'none'.`,
70
+ },
71
+ },
72
+ "pyodide-flag-conflict": {
73
+ severity: "error",
74
+ messages: {
75
+ default:
76
+ "Python is not installed. Please follow https://www.python.org/ to install Python or set 'use-pyodide' to true.",
77
+ },
78
+ },
79
+ // warning
59
80
  "no-valid-client": {
60
81
  severity: "warning",
61
82
  messages: {
@@ -341,13 +341,16 @@ function emitEnum<TServiceOperation extends SdkServiceOperation>(
341
341
  if (!type.isFixed) {
342
342
  types.push(emitBuiltInType(type.valueType));
343
343
  }
344
- return {
344
+
345
+ const newValue = {
345
346
  description: "",
346
347
  internal: true,
347
348
  type: "combined",
348
349
  types,
349
350
  xmlMetadata: {},
350
351
  };
352
+ typesMap.set(type, newValue);
353
+ return newValue;
351
354
  }
352
355
  const values: Record<string, any>[] = [];
353
356
  const name = type.name;
@@ -10,6 +10,7 @@ import {
10
10
  SdkType,
11
11
  } from "@azure-tools/typespec-client-generator-core";
12
12
  import { getNamespaceFullName } from "@typespec/compiler";
13
+ import { marked, Token } from "marked";
13
14
  import { PythonSdkContext } from "./lib.js";
14
15
  import { getSimpleTypeResult, getType } from "./types.js";
15
16
 
@@ -232,3 +233,84 @@ export function getClientNamespace<TServiceOperation extends SdkServiceOperation
232
233
  ? rootNamespace
233
234
  : removeUnderscoresFromNamespace(clientNamespace).toLowerCase();
234
235
  }
236
+
237
+ function parseToken(token: Token): string {
238
+ let parsed = "";
239
+ switch (token.type) {
240
+ case "heading":
241
+ parsed += `${"=".repeat(token.text.length)}\n${token.text}\n${"=".repeat(
242
+ token.text.length,
243
+ )}\n\n`;
244
+ break;
245
+ case "paragraph":
246
+ parsed += `${token.text}\n\n`;
247
+ break;
248
+ case "strong":
249
+ parsed += `**${token.text}**`;
250
+ break;
251
+ case "em":
252
+ parsed += `*${token.text}*`;
253
+ break;
254
+ case "codespan":
255
+ parsed += `\`\`${token.text}\`\``;
256
+ break;
257
+ case "code":
258
+ let codeBlockStyle = token.codeBlockStyle;
259
+ if (codeBlockStyle === undefined) {
260
+ codeBlockStyle = token.raw.split("\n")[0].replace("```", "").trim();
261
+ }
262
+ parsed += `\n\n.. code-block:: ${codeBlockStyle ?? ""}\n\n ${token.text.split("\n").join("\n ")}`;
263
+ break;
264
+ case "link":
265
+ if (token.href !== undefined) {
266
+ parsed += `\`${token.text} <${token.href}>\`_`;
267
+ break;
268
+ }
269
+ parsed += `${token.text}`;
270
+ break;
271
+ case "list":
272
+ if (!token.ordered) {
273
+ parsed += `\n\n${token.items.map((item: any) => `* ${item.text}`).join("\n")}`;
274
+ break;
275
+ }
276
+ parsed += `\n\n${token.items.map((item: any, index: number) => `${index + 1}. ${item.text}`).join("\n")}`;
277
+ break;
278
+ default:
279
+ parsed += token.raw;
280
+ }
281
+ return parsed;
282
+ }
283
+
284
+ export function md2Rst(text?: string): string | undefined {
285
+ try {
286
+ if (!text || text === "") return text;
287
+ const tokens = marked.lexer(text);
288
+ let rst = "";
289
+
290
+ tokens.forEach((token: Token) => {
291
+ if (token.type === "heading") {
292
+ // Heading tokens are block level, so we should check if there are additional tokens inside
293
+ const parsedHeadingText = md2Rst(token.text);
294
+ rst += `${"=".repeat(
295
+ parsedHeadingText!.length,
296
+ )}\n${parsedHeadingText}\n${"=".repeat(parsedHeadingText!.length)}\n\n`;
297
+ } else if ("tokens" in token && token.tokens !== undefined && token.tokens.length > 0) {
298
+ token.tokens.forEach((element: any) => {
299
+ rst += parseToken(element);
300
+ });
301
+ } else {
302
+ rst += parseToken(token);
303
+ }
304
+ });
305
+
306
+ // Trim trailing whitespace or tabs
307
+ return rst.replace(/[ \t]+$/, "");
308
+ } catch (e) {
309
+ if (e instanceof RangeError) {
310
+ // The error is thrown by the tokenizer when the markdown is too long
311
+ // We can ignore it and return the original text
312
+ return text;
313
+ }
314
+ }
315
+ return text;
316
+ }