Skip to content
Draft
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 mock API tests for `service/multiple-services` Spector scenario, covering `ServiceAClient` and `ServiceBClient` with separate versioned operation groups. Fix Python code generator to merge operation groups with the same class name across multiple clients in the same namespace, avoiding duplicate class definitions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname, join, relative, resolve } from "path";

// ---- Shared constants ----

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

export const SpecialFlags: Record<string, Record<string, any>> = {
azure: {
Expand Down Expand Up @@ -135,6 +135,10 @@ export const BASE_AZURE_EMITTER_OPTIONS: Record<
"service/multi-service": {
namespace: "service.multiservice",
},
"service/multiple-services": {
"package-name": "service-multiple-services",
namespace: "service.multipleservices",
},
};

export const BASE_EMITTER_OPTIONS: Record<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,27 @@ def _serialize_and_write_operations_folder(
) -> None:
operations_folder_name = self.code_model.operations_folder_name(namespace)
generation_path = self.code_model.get_generation_dir(namespace)

# Deduplicate operation groups with the same class_name by merging their operations.
# This handles the case where multiple clients share operation group names in the same namespace
# (e.g., both ServiceA and ServiceB have an "Operations" interface).
seen_class_names: dict[str, OperationGroup] = {}
class_operation_groups: list[OperationGroup] = []
for og in operation_groups:
if og.class_name in seen_class_names:
seen_class_names[og.class_name].operations.extend(og.operations)
else:
seen_class_names[og.class_name] = og
class_operation_groups.append(og)

for async_mode, async_path in self.serialize_loop:
prefix_path = f"{async_path}{operations_folder_name}"
# write init file
# write init file (use deduplicated list to avoid duplicate imports)
operations_init_serializer = OperationsInitSerializer(
code_model=self.code_model, operation_groups=operation_groups, env=env, async_mode=async_mode
code_model=self.code_model,
operation_groups=class_operation_groups,
env=env,
async_mode=async_mode,
)
self.write_file(
generation_path / Path(f"{prefix_path}/__init__.py"),
Expand All @@ -367,16 +383,21 @@ def _serialize_and_write_operations_folder(
# write operations file
OgLoop = namedtuple("OgLoop", ["operation_groups", "filename"])
if self.code_model.options["combine-operation-files"]:
# Pass all operation_groups for request builder generation, but class_operation_groups
# for class definitions to avoid duplicate class names in the output.
loops = [OgLoop(operation_groups, "_operations")]
else:
loops = [OgLoop([og], og.filename) for og in operation_groups]
loops = [OgLoop([og], og.filename) for og in class_operation_groups]
for ogs, filename in loops:
operation_group_serializer = OperationGroupsSerializer(
code_model=self.code_model,
operation_groups=ogs,
env=env,
async_mode=async_mode,
client_namespace=namespace,
class_operation_groups=class_operation_groups
if self.code_model.options["combine-operation-files"]
else None,
)
self.write_file(
generation_path / Path(f"{prefix_path}/{filename}.py"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ def __init__(
async_mode: bool,
*,
client_namespace: Optional[str] = None,
class_operation_groups: Optional[list[OperationGroup]] = None,
):
super().__init__(code_model, env, async_mode, client_namespace=client_namespace)
self.operation_groups = operation_groups
self.class_operation_groups = class_operation_groups if class_operation_groups is not None else operation_groups
self.async_mode = async_mode

def _get_request_builders(
Expand Down Expand Up @@ -77,6 +79,7 @@ def serialize(self) -> str:
return template.render(
code_model=self.code_model,
operation_groups=self.operation_groups,
class_operation_groups=self.class_operation_groups,
imports=FileImportSerializer(
imports,
async_mode=self.async_mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
{% endfor %}
{% endfor %}
{% endif %}
{% for operation_group in operation_groups %}
{% for operation_group in class_operation_groups %}
{% include "operation_group.py.jinja2" %}
{% endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -------------------------------------------------------------------------
# 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 azure.core.exceptions import HttpResponseError
from service.multipleservices.aio import ServiceAClient, ServiceBClient
from service.multipleservices.models import VersionsA, VersionsB


@pytest.fixture
def client_a():
return ServiceAClient(endpoint="http://localhost:3000")


@pytest.fixture
def client_b():
return ServiceBClient(endpoint="http://localhost:3000")


@pytest.mark.asyncio
async def test_service_multiple_services_service_a_operations(client_a):
with pytest.raises(HttpResponseError):
async with ServiceAClient(endpoint="http://localhost:3000", api_version=VersionsA.AV1) as wrong_client:
await wrong_client.operations.op_a()

await client_a.operations.op_a()


@pytest.mark.asyncio
async def test_service_multiple_services_service_a_sub_namespace(client_a):
with pytest.raises(HttpResponseError):
async with ServiceAClient(endpoint="http://localhost:3000", api_version=VersionsA.AV1) as wrong_client:
await wrong_client.sub_namespace.sub_op_a()

await client_a.sub_namespace.sub_op_a()


@pytest.mark.asyncio
async def test_service_multiple_services_service_b_operations(client_b):
with pytest.raises(HttpResponseError):
async with ServiceBClient(endpoint="http://localhost:3000", api_version=VersionsB.BV1) as wrong_client:
await wrong_client.operations.op_b()

await client_b.operations.op_b()


@pytest.mark.asyncio
async def test_service_multiple_services_service_b_sub_namespace(client_b):
with pytest.raises(HttpResponseError):
async with ServiceBClient(endpoint="http://localhost:3000", api_version=VersionsB.BV1) as wrong_client:
await wrong_client.sub_namespace.sub_op_b()

await client_b.sub_namespace.sub_op_b()
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -------------------------------------------------------------------------
# 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 azure.core.exceptions import HttpResponseError
from service.multipleservices import ServiceAClient, ServiceBClient
from service.multipleservices.models import VersionsA, VersionsB


@pytest.fixture
def client_a():
with ServiceAClient(endpoint="http://localhost:3000") as client:
yield client


@pytest.fixture
def client_b():
with ServiceBClient(endpoint="http://localhost:3000") as client:
yield client


def test_service_multiple_services_service_a_operations(client_a):
with pytest.raises(HttpResponseError):
with ServiceAClient(endpoint="http://localhost:3000", api_version=VersionsA.AV1) as wrong_client:
wrong_client.operations.op_a()

client_a.operations.op_a()


def test_service_multiple_services_service_a_sub_namespace(client_a):
with pytest.raises(HttpResponseError):
with ServiceAClient(endpoint="http://localhost:3000", api_version=VersionsA.AV1) as wrong_client:
wrong_client.sub_namespace.sub_op_a()

client_a.sub_namespace.sub_op_a()


def test_service_multiple_services_service_b_operations(client_b):
with pytest.raises(HttpResponseError):
with ServiceBClient(endpoint="http://localhost:3000", api_version=VersionsB.BV1) as wrong_client:
wrong_client.operations.op_b()

client_b.operations.op_b()


def test_service_multiple_services_service_b_sub_namespace(client_b):
with pytest.raises(HttpResponseError):
with ServiceBClient(endpoint="http://localhost:3000", api_version=VersionsB.BV1) as wrong_client:
wrong_client.sub_namespace.sub_op_b()

client_b.sub_namespace.sub_op_b()