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
12 changes: 7 additions & 5 deletions instructor/v2/dsl/iterable.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,15 @@ def from_streaming_response(cls, completion) -> Generator[User]:
task_name = name
else:
# Handle `Union[A, B]` / `A | B` task types.
# `types.UnionType` does not have `__name__`, so fall back to a stable name.
task_name = getattr(subtask_class, "__name__", None)
if task_name is None and get_origin(subtask_class) in _UNION_ORIGINS:
# Both `typing.Union[A, B]` and PEP 604 `A | B` expose ``__name__ == "Union"``
# (and `types.UnionType` normalizes to ``typing.Union`` on modern Python), so we
# must detect a union *origin* first and build the name from its members --
# otherwise every union collapses to the unhelpful ``IterableUnion``.
if get_origin(subtask_class) in _UNION_ORIGINS:
members = get_args(subtask_class)
task_name = "Or".join(getattr(m, "__name__", str(m)) for m in members)
if task_name is None:
task_name = str(subtask_class)
else:
task_name = getattr(subtask_class, "__name__", None) or str(subtask_class)

name = f"Iterable{task_name}"

Expand Down
40 changes: 40 additions & 0 deletions tests/dsl/test_simple_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,43 @@ class B(BaseModel):
assert "|" not in prepared.__name__
assert "." not in prepared.__name__
assert prepared.__name__ == "IterableAOrB"


def test_list_of_model_typing_union_generates_clean_name():
"""`List[Union[A, B]]` must derive its Iterable name from the union members.

Both `typing.Union[A, B]` and PEP 604 `A | B` expose ``__name__ == "Union"``, so a
naive ``__name__`` lookup collapses every union to ``IterableUnion``. The name should
instead be built from the member names (``IterableAOrB``).
"""

class A(BaseModel):
x: int

class B(BaseModel):
y: str

prepared = prepare_response_model(List[Union[A, B]]) # noqa: UP006
assert prepared is not None
assert prepared.__name__ == "IterableAOrB"


@pytest.mark.skipif(
sys.version_info < (3, 10),
reason="Union pipe syntax is only available in Python 3.10+",
)
def test_list_of_model_three_way_union_generates_clean_name():
"""A union with more than two members joins every member name with ``Or``."""

class A(BaseModel):
x: int

class B(BaseModel):
y: str

class C(BaseModel):
z: float

prepared = prepare_response_model(list[A | B | C])
assert prepared is not None
assert prepared.__name__ == "IterableAOrBOrC"