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 named single-member unions to emit valid Python type aliases without generating lone overloads.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def __init__(
def imports(self) -> FileImport:
file_import = FileImport(self.code_model)
if self.code_model.named_unions:
file_import.add_submodule_import(
"typing",
"TypeAlias",
ImportType.STDLIB,
)
file_import.add_submodule_import(
"typing",
"Union",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

{{ imports }}
{% for nu in code_model.named_unions %}
{{nu.name}} = {{nu.type_definition()}}
{{nu.name}}: TypeAlias = {{nu.type_definition()}}
{% endfor %}
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ def add_overloads_for_body_param(yaml_data: dict[str, Any], skip_single_body_jso
raw-JSON overload is kept, matching pre-TypedDict behavior).
"""
body_parameter = yaml_data["bodyParameter"]
body_types = body_parameter["type"].get("types", [])
if not (
body_parameter["type"]["type"] == "combined"
and len(yaml_data["bodyParameter"]["type"]["types"]) > len(yaml_data["overloads"])
and len(body_types) > 1
and len(body_types) > len(yaml_data["overloads"])
):
return
for body_type in body_parameter["type"]["types"]:
for body_type in body_types:
if any(o for o in yaml_data["overloads"] if id(o["bodyParameter"]["type"]) == id(body_type)):
continue
if body_type.get("type") == "model" and body_type.get("base") == "json":
Expand Down
41 changes: 40 additions & 1 deletion packages/http-client-python/tests/unit/test_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from jinja2 import PackageLoader, Environment

from pygen.codegen.models import CodeModel, JSONModelType, DPGModelType, build_type
from pygen.codegen.models import CodeModel, CombinedType, JSONModelType, DPGModelType, build_type
from pygen.codegen.models.imports import ImportType, FileImport, TypingSection
from pygen.codegen.models.model_type import TypedDictModelType
from pygen.codegen.models.property import Property
Expand Down Expand Up @@ -522,6 +522,45 @@ def test_unions_serializer_no_unions():
assert "Union" not in output


def test_unions_serializer_single_member_alias():
"""A named single-member union must remain a valid static type alias."""
code_model = _make_code_model(models_mode="dpg")
model = _make_model(code_model, "GenerateVoiceAgentRequest", model_cls=DPGModelType)
named_union = CombinedType(
{"type": "combined", "name": "GenerateAgentRequest"},
code_model,
[model],
)
code_model.named_unions = [named_union]

output = UnionsSerializer(code_model=code_model, env=_make_env()).serialize()

assert "from typing import TYPE_CHECKING, TypeAlias, Union" in output
assert 'GenerateAgentRequest: TypeAlias = "_models.GenerateVoiceAgentRequest"' in output
assert named_union.type_annotation() == '"_unions.GenerateAgentRequest"'


def test_unions_serializer_multiple_member_alias():
"""A named multi-member union remains a Union type alias."""
code_model = _make_code_model(models_mode="dpg")
voice_model = _make_model(code_model, "GenerateVoiceAgentRequest", model_cls=DPGModelType)
text_model = _make_model(code_model, "GenerateTextAgentRequest", model_cls=DPGModelType)
named_union = CombinedType(
{"type": "combined", "name": "GenerateAgentRequest"},
code_model,
[voice_model, text_model],
)
code_model.named_unions = [named_union]

output = UnionsSerializer(code_model=code_model, env=_make_env()).serialize()

assert "from typing import TYPE_CHECKING, TypeAlias, Union" in output
assert (
'GenerateAgentRequest: TypeAlias = Union["_models.GenerateVoiceAgentRequest", '
'"_models.GenerateTextAgentRequest"]' in output
)


# ---------- typed-dict-only ----------


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,52 @@ def _json_model_operation() -> tuple[dict, dict, dict]:
return code_model, yaml_data, model_type


def _named_union_operation(member_count: int) -> dict:
member_types = [{"type": "string"}, {"type": "integer"}]
union_type = {
"type": "combined",
"name": "GenerateAgentRequest",
"types": member_types[:member_count],
}
return {
"name": "generate",
"bodyParameter": {
"wireName": "body",
"clientName": "body",
"location": "body",
"optional": False,
"implementation": "Method",
"contentTypes": ["application/json"],
"type": union_type,
},
"parameters": [_content_type_param()],
"overloads": [],
"responses": [],
"exceptions": [],
}


def test_named_single_member_union_emits_no_overload():
"""The implementation annotation carries the alias without an invalid lone overload."""
yaml_data = _named_union_operation(member_count=1)
named_union = yaml_data["bodyParameter"]["type"]

add_overloads_for_body_param(yaml_data)

assert yaml_data["overloads"] == []
assert yaml_data["bodyParameter"]["type"] is named_union
assert yaml_data["bodyParameter"]["type"]["name"] == "GenerateAgentRequest"


def test_named_multiple_member_union_emits_variant_overloads():
"""Multi-member named unions keep one overload per variant."""
yaml_data = _named_union_operation(member_count=2)

add_overloads_for_body_param(yaml_data)

assert len(yaml_data["overloads"]) == 2


def test_typeddict_only_single_body_emits_no_overload():
"""A lone TypedDict body variant must NOT produce a single ``@overload``."""
plugin = _plugin("typeddict")
Expand Down
Loading