Skip to content

Commit 03b8f4c

Browse files
iscai-msftiscai-msft
andauthored
[python] test infra improvements (#10219)
## Summary Infrastructure overhaul for cleaner code organization and faster CI. ## New Folder Structure ``` http-client-python/ ├── emitter/ # TypeScript emitter (unchanged) ├── generator/pygen/ # Python generator (unchanged) │ ├── tests/ # Consolidated test infrastructure │ ├── tox.ini # Unified tox config with uv for faster installs │ ├── conftest.py # Root pytest fixtures │ ├── requirements/ │ │ ├── base.txt # pytest, pytest-asyncio, coverage │ │ ├── lint.txt # pylint │ │ ├── typecheck.txt # mypy, pyright │ │ ├── docs.txt # sphinx, apiview │ │ ├── azure.txt # azure-core, azure-mgmt-core │ │ └── unbranded.txt # corehttp │ ├── mock_api/ # Mock API tests │ │ ├── azure/ # Azure-specific tests │ │ ├── unbranded/ # Unbranded-specific tests │ │ └── shared/ # Shared test utilities │ ├── unit/ # Unit tests for pygen internals │ └── generated/ # Generated SDK packages │ ├── azure/ # Azure flavor packages │ └── unbranded/ # Unbranded flavor packages │ ├── eng/scripts/ # TypeScript-based tooling │ ├── ci/ │ │ ├── run-tests.ts # Parallel test orchestrator │ │ ├── lint.ts # ESLint + pylint runner │ │ ├── typecheck.ts # mypy + pyright runner │ │ ├── format.ts # Prettier + Black runner │ │ ├── regenerate.ts # SDK regeneration │ │ ├── regenerate-common.ts # Shared regeneration utilities │ │ ├── run_pylint.py # Pylint helper for generated SDKs │ │ ├── run_mypy.py # Mypy helper for generated SDKs │ │ ├── run_pyright.py # Pyright helper for generated SDKs │ │ └── config/ # Tool configurations │ │ ├── pylintrc │ │ ├── mypy.ini │ │ └── pyrightconfig.json │ └── setup/ │ ├── build.ts # Build pygen wheel │ ├── install.ts # Install dependencies │ └── prepare.ts # Prepare environment │ └── dist/ # Compiled emitter output ``` ## Key Changes ### npm Scripts | Command | Description | |---------|-------------| | `npm run test` | Run all tests (emitter + generator) | | `npm run test:emitter` | TypeScript emitter tests (vitest) | | `npm run test:generator` | Python generator tests (tox) | | `npm run lint` | Lint emitter (ESLint) + pygen source (pylint) | | `npm run lint:generated` | Lint generated SDK packages | | `npm run typecheck` | Type check pygen source (mypy + pyright) | | `npm run typecheck:generated` | Type check generated SDK packages | | `npm run format` | Format emitter (Prettier) + pygen (Black) | | `npm run format:generated` | Format generated SDK packages | | `npm run regenerate` | Regenerate all SDK packages | ### Speed Improvements - **uv package installer**: ~5-10x faster than pip for installing generated packages - **Parallel test execution**: Tests run in parallel across CPU cores - **Separated source vs generated validation**: Default commands only run on source code for faster dev loop --------- Co-authored-by: iscai-msft <isabellavcai@gmail.com>
1 parent 051d4b8 commit 03b8f4c

299 files changed

Lines changed: 2637 additions & 928 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@typespec/http-client-python"
5+
---
6+
7+
Clean up test infrastructure, run in parallel, and use uv pip when acceptable. CI runs 2-3 x faster

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ BenchmarkDotnet.Artifacts/
228228
!packages/http-client-java/package-lock.json
229229

230230
# python emitter
231-
packages/http-client-python/generator/test/**/generated/
232-
packages/http-client-python/generator/test/**/cadl-ranch-coverage.json
231+
packages/http-client-python/tests/**/generated/
232+
packages/http-client-python/tests/**/cadl-ranch-coverage.json
233233
!packages/http-client-python/package-lock.json
234234
packages/http-client-python/micropip.lock
235235
packages/http-client-python/venv_build_wheel/

cspell.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ words:
276276
- unassignable
277277
- Uncapitalize
278278
- uncollapsed
279+
- unconfigure
279280
- undifferentiable
280281
- undoc
281282
- Ungroup
@@ -307,6 +308,7 @@ words:
307308
- WHATWG
308309
- WINDOWSARMVMIMAGE
309310
- WINDOWSVMIMAGE
311+
- workerid
310312
- xiangyan
311313
- xiaofei
312314
- xlarge

packages/http-client-python/emitter/src/code-model.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,7 @@ import {
2525
emitPagingHttpMethod,
2626
} from "./http.js";
2727
import { PythonSdkContext } from "./lib.js";
28-
import {
29-
KnownTypes,
30-
disableGenerationMap,
31-
emitEndpointType,
32-
getType,
33-
simpleTypesMap,
34-
typesMap,
35-
} from "./types.js";
28+
import { KnownTypes, emitEndpointType, getType } from "./types.js";
3629
import { emitParamBase, getClientNamespace, getImplementation, getRootNamespace } from "./utils.js";
3730

3831
function emitBasicMethod<TServiceOperation extends SdkServiceOperation>(
@@ -366,7 +359,7 @@ export function emitCodeModel(sdkContext: PythonSdkContext) {
366359
continue;
367360
}
368361
// filter out specific models not used in python, e.g., pageable models
369-
if (disableGenerationMap.has(model)) {
362+
if (sdkContext.__disableGenerationMap.has(model)) {
370363
continue;
371364
}
372365
// filter out core models
@@ -391,7 +384,7 @@ export function emitCodeModel(sdkContext: PythonSdkContext) {
391384
}
392385

393386
// clear usage when a model is only used by paging
394-
for (const type of typesMap.values()) {
387+
for (const type of sdkContext.__typesMap.values()) {
395388
if (
396389
type["type"] === "model" &&
397390
type["referredByOperationType"] === ReferredByOperationTypes.PagingOnly &&
@@ -402,9 +395,9 @@ export function emitCodeModel(sdkContext: PythonSdkContext) {
402395
}
403396

404397
codeModel["types"] = [
405-
...typesMap.values(),
398+
...sdkContext.__typesMap.values(),
406399
...Object.values(KnownTypes),
407-
...simpleTypesMap.values(),
400+
...sdkContext.__simpleTypesMap.values(),
408401
];
409402
codeModel["crossLanguagePackageId"] = ignoreDiagnostics(getCrossLanguagePackageId(sdkContext));
410403
if ((sdkContext.emitContext.options as any).flavor === "azure") {

packages/http-client-python/emitter/src/emitter.ts

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { emitCodeModel } from "./code-model.js";
1010
import { saveCodeModelAsYaml } from "./external-process.js";
1111
import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js";
1212
import { runPython3 } from "./run-python3.js";
13-
import { disableGenerationMap, simpleTypesMap, typesMap } from "./types.js";
1413
import { getRootNamespace, md2Rst } from "./utils.js";
1514

1615
function addDefaultOptions(sdkContext: PythonSdkContext) {
@@ -59,6 +58,9 @@ async function createPythonSdkContext(
5958
return {
6059
...sdkContext,
6160
__endpointPathParameters: [],
61+
__typesMap: new Map(),
62+
__simpleTypesMap: new Map(),
63+
__disableGenerationMap: new Set(),
6264
};
6365
}
6466

@@ -100,12 +102,6 @@ function walkThroughNodes(yamlMap: Record<string, any>): Record<string, any> {
100102
return yamlMap;
101103
}
102104

103-
function cleanAllCache() {
104-
typesMap.clear();
105-
simpleTypesMap.clear();
106-
disableGenerationMap.clear();
107-
}
108-
109105
export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
110106
try {
111107
await onEmitMain(context);
@@ -124,9 +120,6 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
124120
}
125121

126122
async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
127-
// clean all cache to make sure emitter could work in watch mode
128-
cleanAllCache();
129-
130123
const program = context.program;
131124
const sdkContext = await createPythonSdkContext(context);
132125
const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -168,7 +161,7 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
168161
try {
169162
await runPython3(path.join(root, "/eng/scripts/setup/install.py"));
170163
await runPython3(path.join(root, "/eng/scripts/setup/prepare.py"));
171-
} catch (error) {
164+
} catch {
172165
// if the python env is not ready, we use pyodide instead
173166
resolvedOptions["use-pyodide"] = true;
174167
}
@@ -250,7 +243,7 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
250243
execSync(
251244
`${venvPath} -m black --line-length=120 --quiet --fast ${outputDir} --exclude "${excludePattern}"`,
252245
);
253-
checkForPylintIssues(outputDir, excludePattern);
246+
await checkForPylintIssues(outputDir, excludePattern);
254247
}
255248
}
256249
}
@@ -269,7 +262,7 @@ async function setupPyodideCall(root: string) {
269262
if (lockAge > 300) {
270263
fs.unlinkSync(micropipLockPath);
271264
}
272-
} catch (err) {
265+
} catch {
273266
// ignore
274267
}
275268
}
@@ -288,14 +281,14 @@ async function setupPyodideCall(root: string) {
288281
fs.closeSync(fd);
289282
fs.unlinkSync(micropipLockPath);
290283
break;
291-
} catch (err) {
284+
} catch {
292285
await new Promise((resolve) => setTimeout(resolve, 1000));
293286
}
294287
}
295288
return pyodide;
296289
}
297290

298-
function checkForPylintIssues(outputDir: string, excludePattern: string) {
291+
async function checkForPylintIssues(outputDir: string, excludePattern: string) {
299292
const excludeRegex = new RegExp(excludePattern);
300293

301294
const shouldExcludePath = (filePath: string): boolean => {
@@ -304,9 +297,8 @@ function checkForPylintIssues(outputDir: string, excludePattern: string) {
304297
return excludeRegex.test(normalizedPath);
305298
};
306299

307-
const processFile = (filePath: string) => {
308-
let fileContent;
309-
fileContent = fs.readFileSync(filePath, "utf-8");
300+
const processFile = async (filePath: string) => {
301+
let fileContent = await fs.promises.readFile(filePath, "utf-8");
310302
const pylintDisables: string[] = [];
311303
const lineEnding = fileContent.includes("\r\n") && os.platform() === "win32" ? "\r\n" : "\n";
312304
const lines: string[] = fileContent.split(lineEnding);
@@ -321,32 +313,38 @@ function checkForPylintIssues(outputDir: string, excludePattern: string) {
321313
fileContent = lines[0].includes("pylint: disable=")
322314
? [lines[0] + "," + pylintDisables.join(",")].concat(lines.slice(1)).join(lineEnding)
323315
: `# pylint: disable=${pylintDisables.join(",")}${lineEnding}` + fileContent;
316+
await fs.promises.writeFile(filePath, fileContent);
324317
}
325318
}
326-
327-
fs.writeFileSync(filePath, fileContent);
328319
};
329320

330-
const walkDir = (dir: string) => {
321+
const collectPythonFiles = async (dir: string): Promise<string[]> => {
331322
if (shouldExcludePath(dir)) {
332-
return;
323+
return [];
333324
}
334325

335-
const files = fs.readdirSync(dir);
336-
files.forEach((file) => {
337-
const filePath = path.join(dir, file);
326+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
327+
328+
const promises = entries.map(async (entry) => {
329+
const filePath = path.join(dir, entry.name);
338330

339331
if (shouldExcludePath(filePath)) {
340-
return;
332+
return [];
341333
}
342334

343-
if (fs.statSync(filePath).isDirectory()) {
344-
walkDir(filePath);
345-
} else if (file.endsWith(".py")) {
346-
processFile(filePath);
335+
if (entry.isDirectory()) {
336+
return collectPythonFiles(filePath);
337+
} else if (entry.name.endsWith(".py")) {
338+
return [filePath];
347339
}
340+
return [];
348341
});
342+
343+
const results = await Promise.all(promises);
344+
return results.flat();
349345
};
350346

351-
walkDir(outputDir);
347+
// Collect all Python files first, then process in parallel
348+
const pythonFiles = await collectPythonFiles(outputDir);
349+
await Promise.all(pythonFiles.map(processFile));
352350
}

packages/http-client-python/emitter/src/lib.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
SdkContext,
3+
SdkType,
34
UnbrandedSdkEmitterOptions,
45
} from "@azure-tools/typespec-client-generator-core";
56
import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
@@ -28,6 +29,9 @@ export interface PythonEmitterOptions {
2829

2930
export interface PythonSdkContext extends SdkContext<PythonEmitterOptions> {
3031
__endpointPathParameters: Record<string, any>[];
32+
__typesMap: Map<SdkType, Record<string, any>>;
33+
__simpleTypesMap: Map<string | null, Record<string, any>>;
34+
__disableGenerationMap: Set<SdkType>;
3135
}
3236

3337
export const PythonEmitterOptionsSchema: JSONSchemaType<PythonEmitterOptions> = {

packages/http-client-python/emitter/src/system-requirements.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const execute = (
1616
options.onCreate(cp);
1717
}
1818

19-
options.onStdOutData && cp.stdout.on("data", options.onStdOutData);
20-
options.onStdErrData && cp.stderr.on("data", options.onStdErrData);
19+
if (options.onStdOutData) cp.stdout.on("data", options.onStdOutData);
20+
if (options.onStdErrData) cp.stderr.on("data", options.onStdErrData);
2121

2222
let err = "";
2323
let out = "";
@@ -34,7 +34,7 @@ const execute = (
3434
cp.on("error", (err) => {
3535
reject(err);
3636
});
37-
cp.on("close", (code, signal) =>
37+
cp.on("close", (code, _signal) =>
3838
resolve({
3939
stdout: out,
4040
stderr: err,
@@ -110,7 +110,7 @@ const tryPython = async (
110110
`"${PRINT_PYTHON_VERSION_SCRIPT}"`,
111111
]);
112112
return validateVersionRequirement(resolution, result.stdout.trim(), requirement);
113-
} catch (e) {
113+
} catch {
114114
return {
115115
error: true,
116116
...resolution,

0 commit comments

Comments
 (0)