diff --git a/instructor/v2/dsl/iterable.py b/instructor/v2/dsl/iterable.py index c47e92b09..5a0f3bb35 100644 --- a/instructor/v2/dsl/iterable.py +++ b/instructor/v2/dsl/iterable.py @@ -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}" diff --git a/tests/dsl/test_simple_types.py b/tests/dsl/test_simple_types.py index 9e9aca9f3..3202a4099 100644 --- a/tests/dsl/test_simple_types.py +++ b/tests/dsl/test_simple_types.py @@ -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"