Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-python"
---

fix import for _validation.py/_types.py when "generation-subdir" is configured
70 changes: 66 additions & 4 deletions packages/http-client-python/eng/scripts/ci/regenerate-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,19 @@ export const BASE_EMITTER_OPTIONS: Record<
"package-name": "typetest-model-singlediscriminator",
namespace: "typetest.model.singlediscriminator",
},
"type/model/inheritance/recursive": {
"package-name": "typetest-model-recursive",
namespace: "typetest.model.recursive",
},
"type/model/inheritance/recursive": [
{
"package-name": "typetest-model-recursive",
namespace: "typetest.model.recursive",
},
{
// basic test for configuration "generation-subdir"
"package-name": "generation-subdir",
namespace: "generation.subdir",
"generation-subdir": "_generated",
"clear-output-folder": "true",
},
],
"type/model/usage": {
"package-name": "typetest-model-usage",
namespace: "typetest.model.usage",
Expand Down Expand Up @@ -268,6 +277,18 @@ export const BASE_EMITTER_OPTIONS: Record<
"package-name": "specs-documentation",
namespace: "specs.documentation",
},
"versioning/added": [
{
"package-name": "versioning-added",
namespace: "versioning.added",
},
// check whether import of _validation.py/_types.py works when "generation-subdir" is configured
{
"package-name": "generation-subdir2",
namespace: "generation.subdir2",
"generation-subdir": "_generated",
},
],
};

// ---- Shared interfaces ----
Expand Down Expand Up @@ -474,3 +495,44 @@ export async function regenerate(
await runTaskPool(tasks, poolLimit);
}
}

// Preprocess: create files that should be deleted after regeneration (for testing)
export async function preprocess(flavor: string, generatedFolder: string): Promise<void> {
if (flavor === "azure") {
// Use tests/generated/<flavor>/<package> structure (same as output)
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated/azure");

const DELETE_CONTENT = "# This file is to be deleted after regeneration";
const DELETE_FILE = "to_be_deleted.py";
const entries: { folder: string[]; file: string; content: string }[] = [
{
folder: ["authentication-api-key", "authentication", "apikey", "_operations"],
file: DELETE_FILE,
content: DELETE_CONTENT,
},
{
folder: ["generation-subdir", "generation", "subdir", "_generated"],
file: DELETE_FILE,
content: DELETE_CONTENT,
},
{
folder: ["generation-subdir", "generated_tests"],
file: DELETE_FILE,
content: DELETE_CONTENT,
},
{
folder: ["generation-subdir", "generation", "subdir"],
file: "to_be_kept.py",
content: "# This file is to be kept after regeneration",
},
];

await Promise.all(
entries.map(async ({ folder, file, content }) => {
const targetFolder = join(testsGeneratedDir, ...folder);
await promises.mkdir(targetFolder, { recursive: true });
await promises.writeFile(join(targetFolder, file), content);
}),
);
}
}
29 changes: 3 additions & 26 deletions packages/http-client-python/eng/scripts/ci/regenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { compile, NodeHost } from "@typespec/compiler";
import { promises, rmSync } from "fs";
import { rmSync } from "fs";
import { platform } from "os";
import { dirname, join, relative, resolve } from "path";
import pc from "picocolors";
Expand All @@ -17,6 +17,7 @@ import {
BASE_AZURE_EMITTER_OPTIONS,
BASE_EMITTER_OPTIONS,
getSubdirectories,
preprocess,
SpecialFlags,
toPosix,
type RegenerateFlags,
Expand Down Expand Up @@ -108,10 +109,6 @@ const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, st
"package-name": "typetest-array",
namespace: "typetest.array",
},
"type/model/inheritance/recursive": {
"package-name": "typetest-model-recursive",
namespace: "typetest.model.recursive",
},
};

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

// Preprocess: create files that should be deleted after regeneration (for testing)
async function preprocess(flavor: string): Promise<void> {
if (flavor === "azure") {
// Use tests/generated/<flavor>/<package> structure (same as output)
const testsGeneratedDir = resolve(GENERATED_FOLDER, "../tests/generated");
const folderParts = [
"azure",
"authentication-api-key",
"authentication",
"apikey",
"_operations",
];
await promises.mkdir(join(testsGeneratedDir, ...folderParts), { recursive: true });
await promises.writeFile(
join(testsGeneratedDir, ...folderParts, "to_be_deleted.py"),
"# This file is to be deleted after regeneration",
);
}
}

async function regenerateFlavor(
flavor: string,
name: string | undefined,
Expand All @@ -296,7 +273,7 @@ async function regenerateFlavor(
const flags: RegenerateFlags = { flavor, debug, name };

// Preprocess
await preprocess(flavor);
await preprocess(flavor, GENERATED_FOLDER);

// Collect specs
const azureSpecs = flavor === "azure" ? await getSubdirectories(AZURE_HTTP_SPECS, flags) : [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ def _serialize_and_write_utils_folder(self, env: Environment, namespace: str):

def _serialize_and_write_top_level_folder(self, env: Environment, namespace: str) -> None:
root_dir = self.code_model.get_root_dir()
generation_dir = self.code_model.get_generation_dir(namespace)
# write _utils folder
self._serialize_and_write_utils_folder(env, self.code_model.namespace)

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

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

# write _types.py
if self.code_model.named_unions:
self.write_file(
root_dir / Path("_types.py"),
generation_dir / Path("_types.py"),
TypesSerializer(code_model=self.code_model, env=env).serialize(),
)

Expand Down
6 changes: 4 additions & 2 deletions packages/http-client-python/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ def start_server_process():
node_bin = str(ROOT / "node_modules" / ".bin")
env["PATH"] = f"{node_bin}{os.pathsep}{env.get('PATH', '')}"

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

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppressing the mock server’s stdout/stderr makes failures much harder to diagnose (e.g., if the process exits immediately, we only get an exit code without any error output). Consider redirecting output to a log file (and surfacing it on startup failures) rather than sending it to DEVNULL, so CI failures remain actionable while still keeping normal test output clean.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked current log and confirm it is clean and could show enough error traceback



def terminate_server_process(process):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
import pytest
import pytest_asyncio
from generation.subdir2._generated.aio import AddedClient
from generation.subdir2._generated.models import ModelV1, ModelV2, EnumV1, EnumV2


@pytest_asyncio.fixture
async def client():
async with AddedClient(endpoint="http://localhost:3000", version="v2") as client:
yield client


@pytest.mark.asyncio
async def test_v1(client: AddedClient):
assert await client.v1(
ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10),
header_v2="bar",
) == ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10)


@pytest.mark.asyncio
async def test_v2(client: AddedClient):
assert await client.v2(ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")) == ModelV2(
prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar"
)


@pytest.mark.asyncio
async def test_interface_v2(client: AddedClient):
assert await client.interface_v2.v2_in_interface(
ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
) == ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
import pytest
import pytest_asyncio
from generation.subdir._generated.aio import RecursiveClient
from generation.subdir._generated.models import Extension


@pytest_asyncio.fixture
async def client():
async with RecursiveClient() as client:
yield client


@pytest.mark.asyncio
async def test_custom_method(client: RecursiveClient):
assert await client.get() == Extension(
{
"level": 0,
"extension": [{"level": 1, "extension": [{"level": 2}]}, {"level": 1}],
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
import pytest
from generation.subdir2._generated import AddedClient
from generation.subdir2._generated.models import ModelV1, ModelV2, EnumV1, EnumV2


@pytest.fixture
def client():
with AddedClient(endpoint="http://localhost:3000", version="v2") as client:
yield client


def test_v1(client: AddedClient):
assert client.v1(
ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10),
header_v2="bar",
) == ModelV1(prop="foo", enum_prop=EnumV1.ENUM_MEMBER_V2, union_prop=10)


def test_v2(client: AddedClient):
assert client.v2(ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")) == ModelV2(
prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar"
)


def test_interface_v2(client: AddedClient):
assert client.interface_v2.v2_in_interface(
ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
) == ModelV2(prop="foo", enum_prop=EnumV2.ENUM_MEMBER, union_prop="bar")
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from generation.subdir._generated import RecursiveClient
from generation.subdir._generated.models import Extension


def test_custom_method():
client = RecursiveClient()
assert client.get() == Extension(
{
"level": 0,
"extension": [{"level": 1, "extension": [{"level": 2}]}, {"level": 1}],
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,3 @@ def check_sensitive_word(folder: Path, word: str) -> list[str]:
def test_sensitive_word():
check_folder = (Path(os.path.dirname(__file__)) / "../../generated/unbranded").resolve()
assert [] == check_sensitive_word(check_folder, "azure")
# after update spector, it shall also equal to []
expected = [
"authentication-oauth2",
"authentication-noauth-union",
"authentication-union",
"setuppy-authentication-union",
]
if (check_folder / "generation-subdir").exists():
expected.append("generation-subdir")
assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft"))
Loading