@typespec/http-client-python 0.28.3 → 0.29.0

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 (99) hide show
  1. package/dist/emitter/code-model.d.ts.map +1 -1
  2. package/dist/emitter/code-model.js +1 -0
  3. package/dist/emitter/code-model.js.map +1 -1
  4. package/dist/emitter/constants.d.ts +6 -0
  5. package/dist/emitter/constants.d.ts.map +1 -0
  6. package/dist/emitter/constants.js +27 -0
  7. package/dist/emitter/constants.js.map +1 -0
  8. package/dist/emitter/emitter.d.ts.map +1 -1
  9. package/dist/emitter/emitter.js +169 -94
  10. package/dist/emitter/emitter.js.map +1 -1
  11. package/dist/emitter/http.d.ts.map +1 -1
  12. package/dist/emitter/http.js +33 -1
  13. package/dist/emitter/http.js.map +1 -1
  14. package/dist/emitter/lib.d.ts +33 -2
  15. package/dist/emitter/lib.d.ts.map +1 -1
  16. package/dist/emitter/lib.js +17 -0
  17. package/dist/emitter/lib.js.map +1 -1
  18. package/dist/emitter/run-python3.js +1 -1
  19. package/dist/emitter/run-python3.js.map +1 -1
  20. package/dist/emitter/types.d.ts.map +1 -1
  21. package/dist/emitter/types.js +31 -5
  22. package/dist/emitter/types.js.map +1 -1
  23. package/emitter/src/code-model.ts +1 -0
  24. package/emitter/src/constants.ts +27 -0
  25. package/emitter/src/emitter.ts +216 -96
  26. package/emitter/src/http.ts +29 -1
  27. package/emitter/src/lib.ts +20 -0
  28. package/emitter/src/run-python3.ts +1 -1
  29. package/emitter/src/types.ts +35 -5
  30. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  31. package/emitter/test/utils.test.ts +3 -0
  32. package/emitter/tsconfig.build.json +2 -1
  33. package/emitter/tsconfig.json +2 -1
  34. package/eng/scripts/ci/config/eslint-ci.config.mjs +10 -0
  35. package/eng/scripts/ci/config/mypy.ini +1 -1
  36. package/eng/scripts/ci/config/pylintrc +1 -1
  37. package/eng/scripts/ci/config/pyrightconfig.json +1 -1
  38. package/eng/scripts/ci/dev_requirements.txt +1 -1
  39. package/eng/scripts/ci/lint.ts +11 -4
  40. package/eng/scripts/ci/regenerate-common.ts +407 -136
  41. package/eng/scripts/ci/regenerate.ts +103 -194
  42. package/eng/scripts/ci/run-tests.ts +153 -48
  43. package/eng/scripts/ci/run_apiview.py +22 -16
  44. package/eng/scripts/ci/run_mypy.py +28 -18
  45. package/eng/scripts/ci/run_pylint.py +23 -16
  46. package/eng/scripts/ci/run_pyright.py +5 -2
  47. package/eng/scripts/ci/run_sphinx_build.py +24 -9
  48. package/eng/scripts/ci/util.py +61 -25
  49. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  50. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  51. package/eng/scripts/setup/build_pygen_wheel.py +3 -3
  52. package/eng/scripts/setup/install.py +3 -3
  53. package/eng/scripts/setup/install.ts +1 -1
  54. package/eng/scripts/setup/prepare.py +2 -2
  55. package/eng/scripts/setup/run-python3.ts +1 -1
  56. package/eng/scripts/setup/run_batch.py +197 -0
  57. package/eng/scripts/setup/venvtools.py +0 -1
  58. package/generator/build/lib/pygen/__init__.py +1 -3
  59. package/generator/build/lib/pygen/codegen/__init__.py +0 -1
  60. package/generator/build/lib/pygen/codegen/models/base.py +0 -1
  61. package/generator/build/lib/pygen/codegen/models/client.py +2 -1
  62. package/generator/build/lib/pygen/codegen/models/code_model.py +1 -0
  63. package/generator/build/lib/pygen/codegen/models/enum_type.py +0 -1
  64. package/generator/build/lib/pygen/codegen/models/model_type.py +1 -7
  65. package/generator/build/lib/pygen/codegen/models/parameter.py +16 -14
  66. package/generator/build/lib/pygen/codegen/serializers/__init__.py +27 -12
  67. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +12 -3
  68. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +6 -2
  69. package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +17 -4
  70. package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -11
  71. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +19 -7
  72. package/generator/build/lib/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
  73. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
  74. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  75. package/generator/build/lib/pygen/preprocess/__init__.py +37 -30
  76. package/generator/component-detection-pip-report.json +7 -7
  77. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  78. package/generator/pygen/__init__.py +1 -3
  79. package/generator/pygen/codegen/__init__.py +0 -1
  80. package/generator/pygen/codegen/models/base.py +0 -1
  81. package/generator/pygen/codegen/models/client.py +2 -1
  82. package/generator/pygen/codegen/models/code_model.py +1 -0
  83. package/generator/pygen/codegen/models/enum_type.py +0 -1
  84. package/generator/pygen/codegen/models/model_type.py +1 -7
  85. package/generator/pygen/codegen/models/parameter.py +16 -14
  86. package/generator/pygen/codegen/serializers/__init__.py +27 -12
  87. package/generator/pygen/codegen/serializers/builder_serializer.py +12 -3
  88. package/generator/pygen/codegen/serializers/general_serializer.py +6 -2
  89. package/generator/pygen/codegen/serializers/import_serializer.py +17 -4
  90. package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -11
  91. package/generator/pygen/codegen/templates/model_base.py.jinja2 +19 -7
  92. package/generator/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
  93. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
  94. package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  95. package/generator/pygen/preprocess/__init__.py +37 -30
  96. package/generator/pygen.egg-info/PKG-INFO +1 -2
  97. package/generator/pygen.egg-info/requires.txt +1 -1
  98. package/generator/setup.py +1 -2
  99. package/package.json +24 -9
@@ -1,9 +1,80 @@
1
- import { promises } from "fs";
1
+ /* eslint-disable no-console */
2
+ /**
3
+ * Shared helpers, types, constants, and data tables used by `regenerate.ts`.
4
+ *
5
+ * This file is meant to be **byte-identical** between this package and the
6
+ * upstream `@typespec/http-client-python`. typespec-python syncs it from
7
+ * <repo-root>/core/packages/http-client-python/eng/scripts/ci/regenerate-common.ts
8
+ * via `pnpm sync`.
9
+ *
10
+ * Per-repo divergence (paths, emitter name, single-phase vs two-phase
11
+ * orchestration, argv/help text) lives in each repo's own `regenerate.ts`,
12
+ * which builds a `RegenerateContext` and feeds it into the helpers exported
13
+ * from this module.
14
+ */
15
+
16
+ import { compile, NodeHost } from "@typespec/compiler";
17
+ import { execSync } from "child_process";
18
+ import { existsSync, rmSync } from "fs";
19
+ import { access, cp, mkdir, mkdtemp, readdir, writeFile } from "fs/promises";
20
+ import { tmpdir } from "os";
2
21
  import { dirname, join, relative, resolve } from "path";
22
+ import pc from "picocolors";
3
23
 
4
- // ---- Shared constants ----
24
+ // ---- Public types ----
5
25
 
6
- export const SKIP_SPECS: string[] = ["type/file"];
26
+ export interface RegenerateFlags {
27
+ flavor: string;
28
+ debug: boolean;
29
+ name?: string;
30
+ }
31
+
32
+ export interface CompileTask {
33
+ spec: string;
34
+ outputDir: string;
35
+ options: Record<string, unknown>;
36
+ }
37
+
38
+ // Group of tasks for the same spec that must run sequentially
39
+ export interface TaskGroup {
40
+ spec: string;
41
+ tasks: CompileTask[];
42
+ }
43
+
44
+ /**
45
+ * Per-repo context injected into the helpers below. Every value is repo
46
+ * specific and must be supplied by the caller's `regenerate.ts`.
47
+ */
48
+ export interface RegenerateContext {
49
+ /** Absolute path to the package root (the dir containing `package.json`). */
50
+ pluginDir: string;
51
+ /** Absolute path to the azure-http-specs `specs/` dir. */
52
+ azureHttpSpecs: string;
53
+ /** Absolute path to the http-specs `specs/` dir. */
54
+ httpSpecs: string;
55
+ /** Absolute path to where generated SDKs should go (e.g. `<plugin>/generator`). */
56
+ generatedFolder: string;
57
+ /** Emitter name to invoke (e.g. `@azure-tools/typespec-python`). */
58
+ emitterName: string;
59
+ }
60
+
61
+ /**
62
+ * Optional knobs for `buildTaskGroups`. Kept here so the call site in each
63
+ * repo's `regenerate.ts` can opt into the upstream two-phase pipeline
64
+ * (`emitYamlOnly: true`) or the single-phase pipeline (default).
65
+ */
66
+ export interface BuildTaskGroupsOptions {
67
+ /** If true, ask the emitter to write YAML only and skip Python codegen. */
68
+ emitYamlOnly?: boolean;
69
+ }
70
+
71
+ // ---- Public constants ----
72
+
73
+ export const SKIP_SPECS: string[] = [
74
+ "type/file",
75
+ "service/multiple-services",
76
+ "azure/client-generator-core/response-as-bool",
77
+ ];
7
78
 
8
79
  export const SpecialFlags: Record<string, Record<string, any>> = {
9
80
  azure: {
@@ -12,9 +83,9 @@ export const SpecialFlags: Record<string, Record<string, any>> = {
12
83
  },
13
84
  };
14
85
 
15
- // ---- Base emitter options (shared across repos) ----
86
+ // ---- Spec-specific emitter option overrides ----
16
87
 
17
- export const BASE_AZURE_EMITTER_OPTIONS: Record<
88
+ export const AZURE_EMITTER_OPTIONS: Record<
18
89
  string,
19
90
  Record<string, string> | Record<string, string>[]
20
91
  > = {
@@ -48,6 +119,9 @@ export const BASE_AZURE_EMITTER_OPTIONS: Record<
48
119
  "azure/client-generator-core/usage": {
49
120
  namespace: "specs.azure.clientgenerator.core.usage",
50
121
  },
122
+ "azure/client-generator-core/client-doc": {
123
+ namespace: "specs.azure.clientgenerator.core.clientdoc",
124
+ },
51
125
  "azure/client-generator-core/override": {
52
126
  namespace: "specs.azure.clientgenerator.core.override",
53
127
  },
@@ -135,12 +209,13 @@ export const BASE_AZURE_EMITTER_OPTIONS: Record<
135
209
  "service/multi-service": {
136
210
  namespace: "service.multiservice",
137
211
  },
212
+ "client/structure/client-operation-group": {
213
+ "package-name": "client-structure-clientoperationgroup",
214
+ namespace: "client.structure.clientoperationgroup",
215
+ },
138
216
  };
139
217
 
140
- export const BASE_EMITTER_OPTIONS: Record<
141
- string,
142
- Record<string, string> | Record<string, string>[]
143
- > = {
218
+ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, string>[]> = {
144
219
  "resiliency/srv-driven/old.tsp": {
145
220
  "package-name": "resiliency-srv-driven1",
146
221
  namespace: "resiliency.srv.driven1",
@@ -208,10 +283,19 @@ export const BASE_EMITTER_OPTIONS: Record<
208
283
  "package-name": "typetest-model-singlediscriminator",
209
284
  namespace: "typetest.model.singlediscriminator",
210
285
  },
211
- "type/model/inheritance/recursive": {
212
- "package-name": "typetest-model-recursive",
213
- namespace: "typetest.model.recursive",
214
- },
286
+ "type/model/inheritance/recursive": [
287
+ {
288
+ "package-name": "typetest-model-recursive",
289
+ namespace: "typetest.model.recursive",
290
+ },
291
+ {
292
+ "package-name": "generation-subdir",
293
+ namespace: "generation.subdir",
294
+ "generation-subdir": "_generated",
295
+ "generate-test": "false",
296
+ "clear-output-folder": "true",
297
+ },
298
+ ],
215
299
  "type/model/usage": {
216
300
  "package-name": "typetest-model-usage",
217
301
  namespace: "typetest.model.usage",
@@ -268,66 +352,63 @@ export const BASE_EMITTER_OPTIONS: Record<
268
352
  "package-name": "specs-documentation",
269
353
  namespace: "specs.documentation",
270
354
  },
355
+ "versioning/added": [
356
+ {
357
+ "package-name": "versioning-added",
358
+ namespace: "versioning.added",
359
+ },
360
+ {
361
+ "package-name": "generation-subdir2",
362
+ namespace: "generation.subdir2",
363
+ "generate-test": "false",
364
+ "generation-subdir": "_generated",
365
+ },
366
+ ],
271
367
  };
272
368
 
273
- // ---- Shared interfaces ----
274
-
275
- export interface TspCommand {
276
- outputDir: string;
277
- command: string | string[];
278
- }
279
-
280
- export interface RegenerateFlagsInput {
281
- flavor?: string;
282
- debug?: boolean;
283
- name?: string;
284
- pyodide?: boolean;
285
- jobs?: number;
286
- }
287
-
288
- export interface RegenerateFlags {
289
- flavor: string;
290
- debug: boolean;
291
- name?: string;
292
- pyodide?: boolean;
293
- }
369
+ // ---- Public helpers ----
294
370
 
295
- export interface ProcessedEmitterOption {
296
- options: Record<string, string>;
297
- outputDir: string;
371
+ export function toPosix(p: string): string {
372
+ return p.replace(/\\/g, "/");
298
373
  }
299
374
 
300
- export interface RegenerateConfig {
301
- azureHttpSpecs: string;
302
- httpSpecs: string;
303
- emitterOptions: Record<string, Record<string, string> | Record<string, string>[]>;
304
- azureEmitterOptions: Record<string, Record<string, string> | Record<string, string>[]>;
305
- preprocess: (flags: RegenerateFlagsInput) => Promise<void>;
306
- getCmdList: (spec: string, flags: RegenerateFlags) => TspCommand[];
307
- executeCommand: (cmd: TspCommand) => Promise<void>;
375
+ /**
376
+ * Whether a spec path belongs to azure-http-specs (vs standard http-specs).
377
+ * Uses the `azure-http-specs` substring rather than `azure` to avoid false
378
+ * positives when the working-dir path itself contains "azure" (e.g.
379
+ * azure-sdk-for-python).
380
+ */
381
+ export function isAzureSpec(spec: string): boolean {
382
+ return spec.includes("azure-http-specs");
308
383
  }
309
384
 
310
- // ---- Shared utility functions ----
311
-
312
- export function toPosix(dir: string): string {
313
- return dir.replace(/\\/g, "/");
385
+ export function defaultPackageName(spec: string, ctx: RegenerateContext): string {
386
+ const specDir = isAzureSpec(spec) ? ctx.azureHttpSpecs : ctx.httpSpecs;
387
+ return toPosix(relative(specDir, dirname(spec)))
388
+ .replace(/\//g, "-")
389
+ .toLowerCase();
314
390
  }
315
391
 
316
- export function getEmitterOption(
392
+ export function getEmitterOptions(
317
393
  spec: string,
318
394
  flavor: string,
319
- config: RegenerateConfig,
395
+ ctx: RegenerateContext,
320
396
  ): Record<string, string>[] {
321
- const specDir = spec.includes("azure") ? config.azureHttpSpecs : config.httpSpecs;
397
+ const specDir = isAzureSpec(spec) ? ctx.azureHttpSpecs : ctx.httpSpecs;
322
398
  const relativeSpec = toPosix(relative(specDir, spec));
323
399
  const key = relativeSpec.includes("resiliency/srv-driven/old.tsp")
324
400
  ? relativeSpec
325
401
  : dirname(relativeSpec);
326
- const emitter_options = config.emitterOptions[key] ||
327
- (flavor === "azure" ? config.azureEmitterOptions[key] : [{}]) || [{}];
328
- return Array.isArray(emitter_options) ? emitter_options : [emitter_options];
402
+ const emitterOpts = EMITTER_OPTIONS[key] ||
403
+ (flavor === "azure" ? AZURE_EMITTER_OPTIONS[key] : [{}]) || [{}];
404
+ return Array.isArray(emitterOpts) ? emitterOpts : [emitterOpts];
329
405
  }
330
406
 
407
+ /**
408
+ * Walk `baseDir` and collect every TypeSpec entry-point file that the
409
+ * regenerator should compile (handles `client.tsp`, `main.tsp`, and the
410
+ * special `resiliency/srv-driven/old.tsp` case).
411
+ */
331
412
  export async function getSubdirectories(
332
413
  baseDir: string,
333
414
  flags: RegenerateFlags,
@@ -335,7 +416,7 @@ export async function getSubdirectories(
335
416
  const subdirectories: string[] = [];
336
417
 
337
418
  async function searchDir(currentDir: string) {
338
- const items = await promises.readdir(currentDir, { withFileTypes: true });
419
+ const items = await readdir(currentDir, { withFileTypes: true });
339
420
 
340
421
  const promisesArray = items.map(async (item) => {
341
422
  const subDirPath = join(currentDir, item.name);
@@ -347,12 +428,10 @@ export async function getSubdirectories(
347
428
 
348
429
  if (SKIP_SPECS.some((skipSpec) => mainTspRelativePath.includes(skipSpec))) return;
349
430
 
350
- const hasMainTsp = await promises
351
- .access(mainTspPath)
431
+ const hasMainTsp = await access(mainTspPath)
352
432
  .then(() => true)
353
433
  .catch(() => false);
354
- const hasClientTsp = await promises
355
- .access(clientTspPath)
434
+ const hasClientTsp = await access(clientTspPath)
356
435
  .then(() => true)
357
436
  .catch(() => false);
358
437
 
@@ -367,7 +446,6 @@ export async function getSubdirectories(
367
446
  }
368
447
  }
369
448
 
370
- // Recursively search in the subdirectory
371
449
  await searchDir(subDirPath);
372
450
  }
373
451
  });
@@ -379,98 +457,291 @@ export async function getSubdirectories(
379
457
  return subdirectories;
380
458
  }
381
459
 
382
- export function defaultPackageName(spec: string, config: RegenerateConfig): string {
383
- const specDir = spec.includes("azure") ? config.azureHttpSpecs : config.httpSpecs;
384
- return toPosix(relative(specDir, dirname(spec)))
385
- .replace(/\//g, "-")
386
- .toLowerCase();
387
- }
388
-
389
- export function buildOptions(
390
- spec: string,
391
- generatedFolder: string,
460
+ export function buildTaskGroups(
461
+ specs: string[],
392
462
  flags: RegenerateFlags,
393
- config: RegenerateConfig,
394
- ): ProcessedEmitterOption[] {
395
- const results: ProcessedEmitterOption[] = [];
396
- for (const emitterConfig of getEmitterOption(spec, flags.flavor, config)) {
397
- const options: Record<string, string> = { ...emitterConfig };
398
- if (flags.pyodide) {
399
- options["use-pyodide"] = "true";
400
- }
401
- options["flavor"] = flags.flavor;
402
- for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) {
403
- options[k] = v;
404
- }
405
- if (options["emitter-output-dir"] === undefined) {
406
- const packageName = options["package-name"] || defaultPackageName(spec, config);
407
- // Output to new tests/generated/<flavor>/<package> structure
408
- options["emitter-output-dir"] = toPosix(
409
- `${generatedFolder}/../tests/generated/${flags.flavor}/${packageName}`,
410
- );
463
+ ctx: RegenerateContext,
464
+ options: BuildTaskGroupsOptions = {},
465
+ ): TaskGroup[] {
466
+ const groups: TaskGroup[] = [];
467
+
468
+ for (const spec of specs) {
469
+ const tasks: CompileTask[] = [];
470
+
471
+ for (const emitterConfig of getEmitterOptions(spec, flags.flavor, ctx)) {
472
+ // Apply flavor defaults first, then per-spec options so they can override
473
+ // (e.g. "generate-test": "false")
474
+ const opts: Record<string, unknown> = {};
475
+ for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) {
476
+ opts[k] = v;
477
+ }
478
+ Object.assign(opts, emitterConfig);
479
+
480
+ opts["flavor"] = flags.flavor;
481
+
482
+ // Set output directory - tests/generated/<flavor>/<package> structure.
483
+ // Always anchored at <generatedFolder>/../tests/generated regardless of
484
+ // pluginDir, so generator-only checkouts work too.
485
+ const packageName = (opts["package-name"] as string) || defaultPackageName(spec, ctx);
486
+ const outputDir =
487
+ (opts["emitter-output-dir"] as string) ||
488
+ toPosix(`${ctx.generatedFolder}/../tests/generated/${flags.flavor}/${packageName}`);
489
+ opts["emitter-output-dir"] = outputDir;
490
+
491
+ if (flags.debug) {
492
+ opts["debug"] = true;
493
+ }
494
+
495
+ opts["examples-dir"] = toPosix(join(dirname(spec), "examples"));
496
+
497
+ if (options.emitYamlOnly) {
498
+ // Emit YAML only - Python processing is batched after all specs compile.
499
+ opts["emit-yaml-only"] = true;
500
+ }
501
+
502
+ tasks.push({ spec, outputDir, options: opts });
411
503
  }
412
- if (flags.debug) {
413
- options["debug"] = "true";
504
+
505
+ groups.push({ spec, tasks });
506
+ }
507
+
508
+ return groups;
509
+ }
510
+
511
+ export async function compileSpec(
512
+ task: CompileTask,
513
+ ctx: RegenerateContext,
514
+ ): Promise<{ success: boolean; error?: string }> {
515
+ const { spec, outputDir, options } = task;
516
+
517
+ try {
518
+ const compilerOptions = {
519
+ emit: [ctx.pluginDir],
520
+ options: {
521
+ [ctx.emitterName]: options,
522
+ },
523
+ };
524
+
525
+ const program = await compile(NodeHost, spec, compilerOptions);
526
+
527
+ if (program.hasError()) {
528
+ const errors = program.diagnostics
529
+ .filter((d) => d.severity === "error")
530
+ .map((d) => d.message)
531
+ .join("\n");
532
+ return { success: false, error: errors };
414
533
  }
415
- options["examples-dir"] = toPosix(join(dirname(spec), "examples"));
416
- results.push({
417
- options,
418
- outputDir: options["emitter-output-dir"],
419
- });
534
+
535
+ return { success: true };
536
+ } catch (err) {
537
+ rmSync(outputDir, { recursive: true, force: true });
538
+ return { success: false, error: String(err) };
420
539
  }
421
- return results;
422
540
  }
423
541
 
424
- export async function runTaskPool(
425
- tasks: Array<() => Promise<void>>,
426
- poolLimit: number,
427
- ): Promise<void> {
542
+ export function renderProgressBar(
543
+ completed: number,
544
+ failed: number,
545
+ total: number,
546
+ width: number = 40,
547
+ ): string {
548
+ const successCount = completed - failed;
549
+ const successWidth = Math.round((successCount / total) * width);
550
+ const failWidth = Math.round((failed / total) * width);
551
+ const emptyWidth = width - successWidth - failWidth;
552
+
553
+ const successBar = pc.bgGreen(" ".repeat(successWidth));
554
+ const failBar = failed > 0 ? pc.bgRed(" ".repeat(failWidth)) : "";
555
+ const emptyBar = pc.dim("░".repeat(Math.max(0, emptyWidth)));
556
+
557
+ const percent = Math.round((completed / total) * 100);
558
+ return `${successBar}${failBar}${emptyBar} ${pc.cyan(`${percent}%`)} (${completed}/${total})`;
559
+ }
560
+
561
+ export async function runParallel(
562
+ groups: TaskGroup[],
563
+ maxJobs: number,
564
+ ctx: RegenerateContext,
565
+ ): Promise<Map<string, boolean>> {
566
+ const results = new Map<string, boolean>();
428
567
  const executing: Set<Promise<void>> = new Set();
429
568
 
430
- for (const task of tasks) {
431
- // Start the task and remove from set when done
432
- const p: Promise<void> = task().finally(() => executing.delete(p));
569
+ const totalTasks = groups.reduce((sum, g) => sum + g.tasks.length, 0);
570
+ let completed = 0;
571
+ let failed = 0;
572
+ const failedSpecs: string[] = [];
573
+
574
+ const isTTY = process.stdout.isTTY;
575
+
576
+ const updateProgress = () => {
577
+ if (isTTY) {
578
+ process.stdout.write(`\r${renderProgressBar(completed, failed, totalTasks)}`);
579
+ }
580
+ };
581
+
582
+ updateProgress();
583
+
584
+ for (const group of groups) {
585
+ // Each group runs as a unit - tasks within a group run sequentially
586
+ // to avoid state pollution. Different groups run in parallel.
587
+ const runGroup = async () => {
588
+ const specDir = isAzureSpec(group.spec) ? ctx.azureHttpSpecs : ctx.httpSpecs;
589
+ const shortName = toPosix(relative(specDir, dirname(group.spec)));
590
+
591
+ let groupSuccess = true;
592
+ for (const task of group.tasks) {
593
+ const packageName = (task.options["package-name"] as string) || shortName;
594
+
595
+ const result = await compileSpec(task, ctx);
596
+ completed++;
597
+
598
+ if (!result.success) {
599
+ failed++;
600
+ failedSpecs.push(`${packageName}: ${result.error}`);
601
+ groupSuccess = false;
602
+ }
603
+
604
+ updateProgress();
605
+ }
606
+
607
+ results.set(group.spec, groupSuccess);
608
+ };
609
+
610
+ const p = runGroup().finally(() => executing.delete(p));
433
611
  executing.add(p);
434
612
 
435
- // If at capacity, wait for one to complete
436
- if (executing.size >= poolLimit) {
613
+ if (executing.size >= maxJobs) {
437
614
  await Promise.race(executing);
438
615
  }
439
616
  }
440
617
 
441
- // Wait for remaining tasks
442
618
  await Promise.all(executing);
619
+
620
+ if (isTTY) {
621
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
622
+ }
623
+
624
+ if (failedSpecs.length > 0) {
625
+ console.log(pc.red(`\nFailed specs:`));
626
+ for (const spec of failedSpecs) {
627
+ console.log(pc.red(` • ${spec}`));
628
+ }
629
+ }
630
+
631
+ return results;
443
632
  }
444
633
 
445
- export async function regenerate(
446
- flags: RegenerateFlagsInput,
447
- config: RegenerateConfig,
448
- ): Promise<void> {
449
- if (flags.flavor === undefined) {
450
- await regenerate({ flavor: "azure", ...flags }, config);
451
- await regenerate({ flavor: "unbranded", ...flags }, config);
452
- } else {
453
- await config.preprocess(flags);
454
-
455
- const flagsResolved: RegenerateFlags = { debug: false, flavor: flags.flavor, ...flags };
456
- const subdirectoriesForAzure = await getSubdirectories(config.azureHttpSpecs, flagsResolved);
457
- const subdirectoriesForNonAzure = await getSubdirectories(config.httpSpecs, flagsResolved);
458
- const subdirectories =
459
- flags.flavor === "azure"
460
- ? [...subdirectoriesForAzure, ...subdirectoriesForNonAzure]
461
- : subdirectoriesForNonAzure;
462
- const cmdList: TspCommand[] = subdirectories.flatMap((subdirectory) =>
463
- config.getCmdList(subdirectory, flagsResolved),
464
- );
465
-
466
- // Create tasks as functions for the pool
467
- const tasks: Array<() => Promise<void>> = cmdList.map((tspCommand) => {
468
- return () => config.executeCommand(tspCommand);
469
- });
634
+ /**
635
+ * Pre-create the marker files that the test harness expects to find before
636
+ * regeneration so it can verify they're cleared/preserved correctly.
637
+ */
638
+ export async function preprocess(flavor: string, generatedFolder: string): Promise<void> {
639
+ if (flavor !== "azure") return;
640
+
641
+ const testsGeneratedDir = resolve(generatedFolder, "../tests/generated/azure");
642
+
643
+ const DELETE_CONTENT = "# This file is to be deleted after regeneration";
644
+ const KEEP_CONTENT = "# This file is to be kept after regeneration";
645
+ const DELETE_FILE = "to_be_deleted.py";
646
+ const entries: { folder: string[]; file: string; content: string }[] = [
647
+ {
648
+ folder: ["authentication-api-key", "authentication", "apikey", "_operations"],
649
+ file: DELETE_FILE,
650
+ content: DELETE_CONTENT,
651
+ },
652
+ {
653
+ folder: ["generation-subdir", "generation", "subdir", "_generated"],
654
+ file: DELETE_FILE,
655
+ content: DELETE_CONTENT,
656
+ },
657
+ {
658
+ folder: ["generation-subdir", "generated_tests"],
659
+ file: DELETE_FILE,
660
+ content: DELETE_CONTENT,
661
+ },
662
+ {
663
+ folder: ["generation-subdir", "generation", "subdir"],
664
+ file: "to_be_kept.py",
665
+ content: KEEP_CONTENT,
666
+ },
667
+ ];
668
+
669
+ await Promise.all(
670
+ entries.map(async ({ folder, file, content }) => {
671
+ const targetFolder = join(testsGeneratedDir, ...folder);
672
+ await mkdir(targetFolder, { recursive: true });
673
+ await writeFile(join(targetFolder, file), content);
674
+ }),
675
+ );
676
+ }
470
677
 
471
- // Run tasks with a concurrency limit
472
- // Default: 30 jobs, or use provided value
473
- const poolLimit = flags.jobs ?? 30;
474
- await runTaskPool(tasks, poolLimit);
678
+ /**
679
+ * Resets the `tests/generated/{azure,unbranded}` baseline by sparse-checking-out
680
+ * `eng/tools/emitter/gen` from the Azure/azure-sdk-for-python repo, then
681
+ * deleting a couple of fully-generated package folders so regeneration has to
682
+ * recreate them from scratch (smoke test of full-emit path).
683
+ *
684
+ * `generatedFolder` is the per-repo `generator/` directory; baseline lands at
685
+ * `<generatedFolder>/../tests/generated`.
686
+ */
687
+ export async function prepareBaselineOfGeneratedCode(generatedFolder: string): Promise<void> {
688
+ const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
689
+ const branch = "main";
690
+ const sourceSubdir = "eng/tools/emitter/gen";
691
+ const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");
692
+
693
+ console.log(pc.cyan(`\n${"=".repeat(60)}`));
694
+ console.log(pc.cyan(`Resetting baseline from ${repoUrl} (${branch}/${sourceSubdir})`));
695
+ console.log(pc.cyan(`${"=".repeat(60)}\n`));
696
+
697
+ // Wipe tests/generated
698
+ if (existsSync(testsGeneratedDir)) {
699
+ console.log(pc.dim(`Removing ${testsGeneratedDir}`));
700
+ rmSync(testsGeneratedDir, { recursive: true, force: true });
701
+ }
702
+
703
+ // Sparse-checkout the baseline folder into a temp directory
704
+ const tempDir = await mkdtemp(join(tmpdir(), "azsdk-baseline-"));
705
+ try {
706
+ console.log(pc.dim(`Cloning into ${tempDir}`));
707
+ const run = (cmd: string) =>
708
+ execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });
709
+
710
+ run(`git init`);
711
+ run(`git remote add origin ${repoUrl}`);
712
+ run(`git config core.sparseCheckout true`);
713
+ run(`git sparse-checkout init --cone`);
714
+ run(`git sparse-checkout set ${sourceSubdir}`);
715
+ run(`git fetch --depth 1 origin ${branch}`);
716
+ run(`git checkout FETCH_HEAD`);
717
+
718
+ const sourceRoot = join(tempDir, ...sourceSubdir.split("/"));
719
+ for (const flavor of ["azure", "unbranded"]) {
720
+ const src = join(sourceRoot, flavor);
721
+ const dest = join(testsGeneratedDir, flavor);
722
+ if (!existsSync(src)) {
723
+ console.warn(pc.yellow(`Baseline folder not found: ${src}`));
724
+ continue;
725
+ }
726
+ console.log(pc.dim(`Copying ${flavor}/ -> ${dest}`));
727
+ await cp(src, dest, { recursive: true });
728
+ }
729
+
730
+ console.log(pc.green(`Baseline reset complete.\n`));
731
+ } finally {
732
+ rmSync(tempDir, { recursive: true, force: true });
733
+ }
734
+
735
+ // Delete a couple of fully-generated package folders so regeneration has to
736
+ // recreate them from scratch (smoke test of full-emit path).
737
+ const targetsToDelete = [
738
+ join(testsGeneratedDir, "azure", "authentication-http-custom"),
739
+ join(testsGeneratedDir, "unbranded", "encode-array"),
740
+ ];
741
+ for (const target of targetsToDelete) {
742
+ if (existsSync(target)) {
743
+ console.log(pc.dim(`Deleting ${target}`));
744
+ rmSync(target, { recursive: true, force: true });
745
+ }
475
746
  }
476
747
  }