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: internal
packages:
- "@typespec/http-client-python"
---

add baseline of generated code before regenerate
85 changes: 83 additions & 2 deletions packages/http-client-python/eng/scripts/ci/regenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import { compile, NodeHost } from "@typespec/compiler";
import { execSync } from "child_process";
import { existsSync, rmSync } from "fs";
import { access, mkdir, readdir, writeFile } from "fs/promises";
import { platform } from "os";
import { access, cp, mkdir, mkdtemp, readdir, writeFile } from "fs/promises";
import { platform, tmpdir } from "os";
import { dirname, join, relative, resolve } from "path";
import pc from "picocolors";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -424,6 +424,62 @@ async function getSubdirectories(baseDir: string, flags: RegenerateFlags): Promi
return subdirectories;
}

/**
* Resets the local `tests/generated` folder to the baseline checked into
* Azure/azure-sdk-for-python at `eng/tools/emitter/gen`. This ensures
* regeneration starts from a clean, known-good baseline which also contains
* necessary customized code.
*/
async function resetBaselineFromSdkRepo(generatedFolder: string): Promise<void> {
const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
const branch = "main";
const sourceSubdir = "eng/tools/emitter/gen";
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");

console.log(pc.cyan(`\n${"=".repeat(60)}`));
console.log(pc.cyan(`Resetting baseline from ${repoUrl} (${branch}/${sourceSubdir})`));
console.log(pc.cyan(`${"=".repeat(60)}\n`));

// Wipe tests/generated
if (existsSync(testsGeneratedDir)) {
console.log(pc.dim(`Removing ${testsGeneratedDir}`));
rmSync(testsGeneratedDir, { recursive: true, force: true });
}
await mkdir(testsGeneratedDir, { recursive: true });

// Sparse-checkout the baseline folder into a temp directory
const tempDir = await mkdtemp(join(tmpdir(), "azsdk-baseline-"));
try {
console.log(pc.dim(`Cloning into ${tempDir}`));
const run = (cmd: string) =>
execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });

run(`git init`);
run(`git remote add origin ${repoUrl}`);
run(`git config core.sparseCheckout true`);
run(`git sparse-checkout init --cone`);
run(`git sparse-checkout set ${sourceSubdir}`);
run(`git fetch --depth 1 origin ${branch}`);
run(`git checkout FETCH_HEAD`);

const sourceRoot = join(tempDir, ...sourceSubdir.split("/"));
for (const flavor of ["azure", "unbranded"]) {
const src = join(sourceRoot, flavor);
const dest = join(testsGeneratedDir, flavor);
if (!existsSync(src)) {
console.warn(pc.yellow(`Baseline folder not found: ${src}`));
continue;
}
console.log(pc.dim(`Copying ${flavor}/ -> ${dest}`));
await cp(src, dest, { recursive: true });
}

console.log(pc.green(`Baseline reset complete.\n`));
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
}

async function preprocess(flavor: string, generatedFolder: string): Promise<void> {
if (flavor === "azure") {
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated/azure");
Expand Down Expand Up @@ -805,6 +861,29 @@ async function regenerateFlavor(
return pySuccess;
}

/**
* Deletes a couple of fully-generated package folders from the baseline so that
* regeneration has to recreate them from scratch.
*/
function deleteSomeGeneratedFiles() {
const testsGeneratedDir = resolve(GENERATED_FOLDER, "../tests/generated");
const targets = [
join(testsGeneratedDir, "azure", "authentication-http-custom"),
join(testsGeneratedDir, "unbranded", "encode-array"),
];
for (const target of targets) {
if (existsSync(target)) {
console.log(pc.dim(`Deleting ${target}`));
rmSync(target, { recursive: true, force: true });
}
}
}

async function preProcess() {
await resetBaselineFromSdkRepo(GENERATED_FOLDER);
deleteSomeGeneratedFiles();
}

async function main() {
const isWindows = platform() === "win32";
const flavor = argv.values.flavor;
Expand All @@ -827,6 +906,8 @@ async function main() {
const startTime = performance.now();
let success: boolean;

await preProcess();

if (flavor) {
success = await regenerateFlavor(flavor, name, debug, jobs);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ def _coerce(value):
return False
return value

pygen_args = {
k: _coerce(v) for k, v in command_args.items() if k not in ["emit-yaml-only"]
}
pygen_args = {k: _coerce(v) for k, v in command_args.items() if k not in ["emit-yaml-only"]}

# Run preprocess and codegen (black is batched at the end for performance)
preprocess.PreProcessPlugin(output_folder=output_dir, tsp_file=yaml_path, **pygen_args).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.aio import AddedClient
from generation.subdir2 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,19 @@
# -------------------------------------------------------------------------
# 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.subdir import Extension
from generation.subdir.aio import CustomizedClient


@pytest.mark.asyncio
async def test_custom_method():
client = CustomizedClient()
assert (await client.customized_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,32 @@
# -------------------------------------------------------------------------
# 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 import AddedClient, 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,16 @@
# -------------------------------------------------------------------------
# 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 import CustomizedClient, Extension


def test_custom_method():
client = CustomizedClient()
assert client.customized_get() == Extension(
{
"level": 0,
"extension": [{"level": 1, "extension": [{"level": 2}]}, {"level": 1}],
}
)
12 changes: 12 additions & 0 deletions packages/http-client-python/tests/mock_api/shared/test_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------


def test_patch_mixin_operation_group_in_operations_folder(key_credential):
from authentication.apikey import ApiKeyClient, aio

assert hasattr(ApiKeyClient(key_credential), "patch_added_operation")
assert hasattr(aio.ApiKeyClient(key_credential), "patch_added_operation")
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from pathlib import Path
from typing import Dict, Any

try:
import tomllib
except ImportError:
import tomli as tomllib


def check_no_setup_py(package_path: str) -> None:
"""
Check that setup.py does not exist in the package directory.

Args:
package_path: Relative path to the package directory

Raises:
AssertionError: If setup.py exists in the package directory
"""
package_dir = Path(__file__).parent / package_path
setup_py_path = package_dir / "setup.py"

assert not setup_py_path.exists(), f"setup.py should not exist at {setup_py_path} when using pyproject.toml"


def get_pyproject_section(package_path: str, section_name: str) -> Dict[str, Any]:
"""
Get a specific section from a package's pyproject.toml file.

Args:
package_path: Relative path to the package directory containing pyproject.toml
section_name: Dot-separated section name (e.g., "tool.azure-sdk-build")

Returns:
Dictionary containing the section data

Raises:
AssertionError: If pyproject.toml not found or section missing
"""
try:
# Convert to absolute path and find pyproject.toml
package_dir = Path(__file__).parent / package_path
pyproject_path = package_dir / "pyproject.toml"

# Assert pyproject.toml exists
assert pyproject_path.exists(), f"pyproject.toml not found at {pyproject_path}"

# Parse pyproject.toml
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)

# Check that the project name matches the folder name
if "project" in data and "name" in data["project"]:
expected_name = package_dir.name
actual_name = data["project"]["name"]
assert (
actual_name == expected_name
), f"Project name '{actual_name}' in pyproject.toml does not match folder name '{expected_name}'"

# Navigate to the requested section
section_parts = section_name.split(".")
current_data = data

for part in section_parts:
assert (
part in current_data
), f"pyproject.toml does not contain [{'.'.join(section_parts[:section_parts.index(part)+1])}] section"
current_data = current_data[part]

return current_data

except Exception as e:
raise AssertionError(f"Error checking pyproject.toml at '{package_path}': {e}")


def test_azure_sdk_build():
"""Test that authentication-union packages have pyproject.toml with [tool.azure-sdk-build] pyright = false."""

# Need to check the file directly, since installed distribution metadata won't include custom sections.
test_paths = ["../../../generated/azure/authentication-union"]

for package_path in test_paths:
# First check that setup.py doesn't exist
check_no_setup_py(package_path)

# Get the [tool.azure-sdk-build] section
azure_sdk_build = get_pyproject_section(package_path, "tool.azure-sdk-build")

# Check for pyright = false
assert (
"pyright" in azure_sdk_build
), f"[tool.azure-sdk-build] section does not contain 'pyright' setting in {package_path}"
assert (
azure_sdk_build["pyright"] is False
), f"Expected pyright = false, but got pyright = {azure_sdk_build['pyright']} in {package_path}"
Loading