Skip to content

Commit 3e428bb

Browse files
authored
Fix CI to run pylint/mypy/pyright on generated SDK code (#10336)
1 parent c6a6519 commit 3e428bb

10 files changed

Lines changed: 129 additions & 107 deletions

File tree

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+
fix ci to add lint check for generated sdk code

packages/http-client-python/eng/scripts/Test-Packages.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ try {
7474

7575
Invoke-LoggedCommand "npm run ci"
7676
Write-Host "All tests passed." -ForegroundColor Green
77+
78+
# Linux specific: check mypy/lint/pyright on generated code
79+
if ($IsLinux) {
80+
Write-Host "`n=== Running lint on generated code ===" -ForegroundColor Cyan
81+
Invoke-LoggedCommand "npm run lint:generated"
82+
Write-Host "Generated code checks passed." -ForegroundColor Green
83+
84+
Write-Host "`n=== Running mypy/pyright on generated code ===" -ForegroundColor Cyan
85+
Invoke-LoggedCommand "npm run typecheck:generated"
86+
Write-Host "Generated code mypy/pyright checks passed." -ForegroundColor Green
87+
}
7788
}
7889
}
7990
finally {

packages/http-client-python/eng/scripts/ci/regenerate-common.ts

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { dirname, join, relative, resolve } from "path";
33

44
// ---- Shared constants ----
55

6-
export const SKIP_SPECS: string[] = ["type/file"];
6+
export const SKIP_SPECS: string[] = ["type/file", "service/multiple-services"];
77

88
export const SpecialFlags: Record<string, Record<string, any>> = {
99
azure: {
@@ -218,6 +218,7 @@ export const BASE_EMITTER_OPTIONS: Record<
218218
"package-name": "generation-subdir",
219219
namespace: "generation.subdir",
220220
"generation-subdir": "_generated",
221+
"generate-test": "false",
221222
"clear-output-folder": "true",
222223
},
223224
],
@@ -286,6 +287,7 @@ export const BASE_EMITTER_OPTIONS: Record<
286287
{
287288
"package-name": "generation-subdir2",
288289
namespace: "generation.subdir2",
290+
"generate-test": "false",
289291
"generation-subdir": "_generated",
290292
},
291293
],
@@ -313,11 +315,6 @@ export interface RegenerateFlags {
313315
pyodide?: boolean;
314316
}
315317

316-
export interface ProcessedEmitterOption {
317-
options: Record<string, string>;
318-
outputDir: string;
319-
}
320-
321318
export interface RegenerateConfig {
322319
azureHttpSpecs: string;
323320
httpSpecs: string;
@@ -400,48 +397,6 @@ export async function getSubdirectories(
400397
return subdirectories;
401398
}
402399

403-
export function defaultPackageName(spec: string, config: RegenerateConfig): string {
404-
const specDir = spec.includes("azure") ? config.azureHttpSpecs : config.httpSpecs;
405-
return toPosix(relative(specDir, dirname(spec)))
406-
.replace(/\//g, "-")
407-
.toLowerCase();
408-
}
409-
410-
export function buildOptions(
411-
spec: string,
412-
generatedFolder: string,
413-
flags: RegenerateFlags,
414-
config: RegenerateConfig,
415-
): ProcessedEmitterOption[] {
416-
const results: ProcessedEmitterOption[] = [];
417-
for (const emitterConfig of getEmitterOption(spec, flags.flavor, config)) {
418-
const options: Record<string, string> = { ...emitterConfig };
419-
if (flags.pyodide) {
420-
options["use-pyodide"] = "true";
421-
}
422-
options["flavor"] = flags.flavor;
423-
for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) {
424-
options[k] = v;
425-
}
426-
if (options["emitter-output-dir"] === undefined) {
427-
const packageName = options["package-name"] || defaultPackageName(spec, config);
428-
// Output to new tests/generated/<flavor>/<package> structure
429-
options["emitter-output-dir"] = toPosix(
430-
`${generatedFolder}/../tests/generated/${flags.flavor}/${packageName}`,
431-
);
432-
}
433-
if (flags.debug) {
434-
options["debug"] = "true";
435-
}
436-
options["examples-dir"] = toPosix(join(dirname(spec), "examples"));
437-
results.push({
438-
options,
439-
outputDir: options["emitter-output-dir"],
440-
});
441-
}
442-
return results;
443-
}
444-
445400
export async function runTaskPool(
446401
tasks: Array<() => Promise<void>>,
447402
poolLimit: number,

packages/http-client-python/eng/scripts/ci/regenerate.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,15 @@ function buildTaskGroups(specs: string[], flags: RegenerateFlags): TaskGroup[] {
148148
const tasks: CompileTask[] = [];
149149

150150
for (const emitterConfig of getEmitterOptions(spec, flags.flavor)) {
151-
const options: Record<string, unknown> = { ...emitterConfig };
152-
153-
// Add flavor-specific options
154-
options["flavor"] = flags.flavor;
151+
// Apply flavor defaults first, then per-spec options so they can override (e.g., "generate-test": "false")
152+
const options: Record<string, unknown> = {};
155153
for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) {
156154
options[k] = v;
157155
}
156+
Object.assign(options, emitterConfig);
157+
158+
// Add flavor
159+
options["flavor"] = flags.flavor;
158160

159161
// Set output directory - use tests/generated/<flavor>/<package> structure
160162
const packageName = (options["package-name"] as string) || defaultPackageName(spec);

packages/http-client-python/eng/scripts/ci/run_mypy.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ def get_config_file_location():
2727

2828

2929
def _single_dir_mypy(mod):
30-
inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info"))
30+
# Exclude "build" directories to avoid mypy "Duplicate module" errors caused by
31+
# stale build/lib/ artifacts from previous setup.py builds.
32+
inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info") and d.stem != "build")
3133
try:
3234
check_call(
3335
[

packages/http-client-python/eng/scripts/ci/run_pylint.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,30 @@ def get_rfc_file_location():
2727

2828

2929
def _single_dir_pylint(mod):
30-
inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info"))
30+
# Exclude "build" directories created by pip install / setup.py build.
31+
# Without this, "build" may be picked first alphabetically and pylint would
32+
# lint stale build artifacts instead of the actual source, causing false
33+
# positives (e.g. modules named "json", "xml", "datetime" shadow the stdlib).
34+
inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info") and d.name != "build")
35+
# Only load the Azure pylint guidelines checker plugin for azure packages.
36+
# The plugin (azure-pylint-guidelines-checker) is only installed in the
37+
# lint-azure tox environment and is not available for unbranded packages.
38+
is_azure = "azure" in mod.parts
39+
pylint_args = [
40+
sys.executable,
41+
"-m",
42+
"pylint",
43+
"--rcfile={}".format(get_rfc_file_location()),
44+
"--evaluation=(max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention + info)/ statement) * 10)))",
45+
"--output-format=parseable",
46+
"--recursive=y",
47+
"--py-version=3.9",
48+
]
49+
if is_azure:
50+
pylint_args.append("--load-plugins=pylint_guidelines_checker")
51+
pylint_args.append(str(inner_class.absolute()))
3152
try:
32-
check_call(
33-
[
34-
sys.executable,
35-
"-m",
36-
"pylint",
37-
"--rcfile={}".format(get_rfc_file_location()),
38-
"--evaluation=(max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention + info)/ statement) * 10)))",
39-
"--load-plugins=pylint_guidelines_checker",
40-
"--output-format=parseable",
41-
"--recursive=y",
42-
"--py-version=3.9",
43-
str(inner_class.absolute()),
44-
]
45-
)
53+
check_call(pylint_args)
4654
return True
4755
except CalledProcessError as e:
4856
logging.error("{} exited with linting error {}".format(str(inner_class.absolute()), e.returncode))

packages/http-client-python/eng/scripts/ci/util.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
logging.getLogger().setLevel(logging.INFO)
1414

15-
ROOT_FOLDER = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "generator"))
15+
ROOT_FOLDER = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "tests"))
1616

1717
IGNORE_FOLDER = []
1818

@@ -53,11 +53,12 @@ def run_check(name, call_back, log_info):
5353

5454
args = parser.parse_args()
5555

56-
pkg_dir = Path(ROOT_FOLDER) / Path("test") / Path(args.test_folder)
57-
if args.generator:
58-
pkg_dir /= Path(args.generator)
56+
pkg_dir = Path(ROOT_FOLDER)
5957
if args.subfolder:
6058
pkg_dir /= Path(args.subfolder)
59+
pkg_dir /= Path(args.test_folder)
60+
if args.generator:
61+
pkg_dir /= Path(args.generator)
6162
dirs = [d for d in pkg_dir.iterdir() if d.is_dir() and not d.stem.startswith("_") and d.stem not in IGNORE_FOLDER]
6263
if args.file_name:
6364
dirs = [d for d in dirs if args.file_name.lower() in d.stem.lower()]

0 commit comments

Comments
 (0)