Skip to content

Commit 4ba1822

Browse files
msyycCopilot
andauthored
[python] Fix import for generation-subdir (#10303)
## Description Fix import generation when using a generation subdirectory (generation-subdir). This ensures imports are correctly resolved for generated code in subdirectories. ## Changes - Fix import resolution in `generator/pygen/codegen/serializers/__init__.py` - Update `regenerate-common.ts` script - Add test cases for generation-subdir scenarios ## Changelog ``` changeKind: fix packages: - @typespec/http-client-python ``` --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a6d0b97 commit 4ba1822

10 files changed

Lines changed: 197 additions & 44 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/http-client-python"
5+
---
6+
7+
fix import for _validation.py/_types.py when "generation-subdir" is configured

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,19 @@ export const BASE_EMITTER_OPTIONS: Record<
208208
"package-name": "typetest-model-singlediscriminator",
209209
namespace: "typetest.model.singlediscriminator",
210210
},
211-
"type/model/inheritance/recursive": {
212-
"package-name": "typetest-model-recursive",
213-
namespace: "typetest.model.recursive",
214-
},
211+
"type/model/inheritance/recursive": [
212+
{
213+
"package-name": "typetest-model-recursive",
214+
namespace: "typetest.model.recursive",
215+
},
216+
{
217+
// basic test for configuration "generation-subdir"
218+
"package-name": "generation-subdir",
219+
namespace: "generation.subdir",
220+
"generation-subdir": "_generated",
221+
"clear-output-folder": "true",
222+
},
223+
],
215224
"type/model/usage": {
216225
"package-name": "typetest-model-usage",
217226
namespace: "typetest.model.usage",
@@ -268,6 +277,18 @@ export const BASE_EMITTER_OPTIONS: Record<
268277
"package-name": "specs-documentation",
269278
namespace: "specs.documentation",
270279
},
280+
"versioning/added": [
281+
{
282+
"package-name": "versioning-added",
283+
namespace: "versioning.added",
284+
},
285+
// check whether import of _validation.py/_types.py works when "generation-subdir" is configured
286+
{
287+
"package-name": "generation-subdir2",
288+
namespace: "generation.subdir2",
289+
"generation-subdir": "_generated",
290+
},
291+
],
271292
};
272293

273294
// ---- Shared interfaces ----
@@ -474,3 +495,44 @@ export async function regenerate(
474495
await runTaskPool(tasks, poolLimit);
475496
}
476497
}
498+
499+
// Preprocess: create files that should be deleted after regeneration (for testing)
500+
export async function preprocess(flavor: string, generatedFolder: string): Promise<void> {
501+
if (flavor === "azure") {
502+
// Use tests/generated/<flavor>/<package> structure (same as output)
503+
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated/azure");
504+
505+
const DELETE_CONTENT = "# This file is to be deleted after regeneration";
506+
const DELETE_FILE = "to_be_deleted.py";
507+
const entries: { folder: string[]; file: string; content: string }[] = [
508+
{
509+
folder: ["authentication-api-key", "authentication", "apikey", "_operations"],
510+
file: DELETE_FILE,
511+
content: DELETE_CONTENT,
512+
},
513+
{
514+
folder: ["generation-subdir", "generation", "subdir", "_generated"],
515+
file: DELETE_FILE,
516+
content: DELETE_CONTENT,
517+
},
518+
{
519+
folder: ["generation-subdir", "generated_tests"],
520+
file: DELETE_FILE,
521+
content: DELETE_CONTENT,
522+
},
523+
{
524+
folder: ["generation-subdir", "generation", "subdir"],
525+
file: "to_be_kept.py",
526+
content: "# This file is to be kept after regeneration",
527+
},
528+
];
529+
530+
await Promise.all(
531+
entries.map(async ({ folder, file, content }) => {
532+
const targetFolder = join(testsGeneratedDir, ...folder);
533+
await promises.mkdir(targetFolder, { recursive: true });
534+
await promises.writeFile(join(targetFolder, file), content);
535+
}),
536+
);
537+
}
538+
}

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

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { compile, NodeHost } from "@typespec/compiler";
10-
import { promises, rmSync } from "fs";
10+
import { rmSync } from "fs";
1111
import { platform } from "os";
1212
import { dirname, join, relative, resolve } from "path";
1313
import pc from "picocolors";
@@ -17,6 +17,7 @@ import {
1717
BASE_AZURE_EMITTER_OPTIONS,
1818
BASE_EMITTER_OPTIONS,
1919
getSubdirectories,
20+
preprocess,
2021
SpecialFlags,
2122
toPosix,
2223
type RegenerateFlags,
@@ -108,10 +109,6 @@ const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, st
108109
"package-name": "typetest-array",
109110
namespace: "typetest.array",
110111
},
111-
"type/model/inheritance/recursive": {
112-
"package-name": "typetest-model-recursive",
113-
namespace: "typetest.model.recursive",
114-
},
115112
};
116113

117114
interface CompileTask {
@@ -263,26 +260,6 @@ async function runParallel(groups: TaskGroup[], maxJobs: number): Promise<Map<st
263260
return results;
264261
}
265262

266-
// Preprocess: create files that should be deleted after regeneration (for testing)
267-
async function preprocess(flavor: string): Promise<void> {
268-
if (flavor === "azure") {
269-
// Use tests/generated/<flavor>/<package> structure (same as output)
270-
const testsGeneratedDir = resolve(GENERATED_FOLDER, "../tests/generated");
271-
const folderParts = [
272-
"azure",
273-
"authentication-api-key",
274-
"authentication",
275-
"apikey",
276-
"_operations",
277-
];
278-
await promises.mkdir(join(testsGeneratedDir, ...folderParts), { recursive: true });
279-
await promises.writeFile(
280-
join(testsGeneratedDir, ...folderParts, "to_be_deleted.py"),
281-
"# This file is to be deleted after regeneration",
282-
);
283-
}
284-
}
285-
286263
async function regenerateFlavor(
287264
flavor: string,
288265
name: string | undefined,
@@ -296,7 +273,7 @@ async function regenerateFlavor(
296273
const flags: RegenerateFlags = { flavor, debug, name };
297274

298275
// Preprocess
299-
await preprocess(flavor);
276+
await preprocess(flavor, GENERATED_FOLDER);
300277

301278
// Collect specs
302279
const azureSpecs = flavor === "azure" ? await getSubdirectories(AZURE_HTTP_SPECS, flags) : [];

packages/http-client-python/generator/pygen/codegen/serializers/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def _serialize_and_write_utils_folder(self, env: Environment, namespace: str):
482482

483483
def _serialize_and_write_top_level_folder(self, env: Environment, namespace: str) -> None:
484484
root_dir = self.code_model.get_root_dir()
485+
generation_dir = self.code_model.get_generation_dir(namespace)
485486
# write _utils folder
486487
self._serialize_and_write_utils_folder(env, self.code_model.namespace)
487488

@@ -498,16 +499,18 @@ def _serialize_and_write_top_level_folder(self, env: Environment, namespace: str
498499
self.write_file(root_dir / Path("py.typed"), pytyped_value)
499500

500501
# write _validation.py
502+
# Use generation_dir so that relative imports from operations/clients
503+
# within a generation-subdir resolve correctly.
501504
if any(og for client in self.code_model.clients for og in client.operation_groups if og.need_validation):
502505
self.write_file(
503-
root_dir / Path("_validation.py"),
506+
generation_dir / Path("_validation.py"),
504507
general_serializer.serialize_validation_file(),
505508
)
506509

507510
# write _types.py
508511
if self.code_model.named_unions:
509512
self.write_file(
510-
root_dir / Path("_types.py"),
513+
generation_dir / Path("_types.py"),
511514
TypesSerializer(code_model=self.code_model, env=env).serialize(),
512515
)
513516

packages/http-client-python/tests/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ def start_server_process():
7373
node_bin = str(ROOT / "node_modules" / ".bin")
7474
env["PATH"] = f"{node_bin}{os.pathsep}{env.get('PATH', '')}"
7575

76+
# Suppress server stdout/stderr to avoid confusing "Request validation failed" warnings
77+
# in test output. Server readiness is validated via HTTP polling in wait_for_server().
7678
if os.name == "nt":
77-
return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env)
78-
return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, preexec_fn=os.setsid)
79+
return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
80+
return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, preexec_fn=os.setsid, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
7981

8082

8183
def terminate_server_process(process):
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
import pytest
7+
import pytest_asyncio
8+
from generation.subdir2._generated.aio import AddedClient
9+
from generation.subdir2._generated.models import ModelV1, ModelV2, EnumV1, EnumV2
10+
11+
12+
@pytest_asyncio.fixture
13+
async def client():
14+
async with AddedClient(endpoint="http://localhost:3000", version="v2") as client:
15+
yield client
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_v1(client: AddedClient):
20+
assert await client.v1(
21+
ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10),
22+
header_v2="bar",
23+
) == ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10)
24+
25+
26+
@pytest.mark.asyncio
27+
async def test_v2(client: AddedClient):
28+
assert await client.v2(ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")) == ModelV2(
29+
prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar"
30+
)
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_interface_v2(client: AddedClient):
35+
assert await client.interface_v2.v2_in_interface(
36+
ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
37+
) == ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
import pytest
7+
import pytest_asyncio
8+
from generation.subdir._generated.aio import RecursiveClient
9+
from generation.subdir._generated.models import Extension
10+
11+
12+
@pytest_asyncio.fixture
13+
async def client():
14+
async with RecursiveClient() as client:
15+
yield client
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_custom_method(client: RecursiveClient):
20+
assert await client.get() == Extension(
21+
{
22+
"level": 0,
23+
"extension": [{"level": 1, "extension": [{"level": 2}]}, {"level": 1}],
24+
}
25+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
import pytest
7+
from generation.subdir2._generated import AddedClient
8+
from generation.subdir2._generated.models import ModelV1, ModelV2, EnumV1, EnumV2
9+
10+
11+
@pytest.fixture
12+
def client():
13+
with AddedClient(endpoint="http://localhost:3000", version="v2") as client:
14+
yield client
15+
16+
17+
def test_v1(client: AddedClient):
18+
assert client.v1(
19+
ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10),
20+
header_v2="bar",
21+
) == ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10)
22+
23+
24+
def test_v2(client: AddedClient):
25+
assert client.v2(ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")) == ModelV2(
26+
prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar"
27+
)
28+
29+
30+
def test_interface_v2(client: AddedClient):
31+
assert client.interface_v2.v2_in_interface(
32+
ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
33+
) == ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
from generation.subdir._generated import RecursiveClient
7+
from generation.subdir._generated.models import Extension
8+
9+
10+
def test_custom_method():
11+
client = RecursiveClient()
12+
assert client.get() == Extension(
13+
{
14+
"level": 0,
15+
"extension": [{"level": 1, "extension": [{"level": 2}]}, {"level": 1}],
16+
}
17+
)

packages/http-client-python/tests/mock_api/unbranded/test_unbranded.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,3 @@ def check_sensitive_word(folder: Path, word: str) -> list[str]:
5555
def test_sensitive_word():
5656
check_folder = (Path(os.path.dirname(__file__)) / "../../generated/unbranded").resolve()
5757
assert [] == check_sensitive_word(check_folder, "azure")
58-
# after update spector, it shall also equal to []
59-
expected = [
60-
"authentication-oauth2",
61-
"authentication-noauth-union",
62-
"authentication-union",
63-
"setuppy-authentication-union",
64-
]
65-
if (check_folder / "generation-subdir").exists():
66-
expected.append("generation-subdir")
67-
assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft"))

0 commit comments

Comments
 (0)