fix: allow union types (X | Y) for stream_type in streaming_action.pydantic#845
fix: allow union types (X | Y) for stream_type in streaming_action.pydantic#845mirakour wants to merge 3 commits into
Conversation
Add regression tests for union stream type support in Pydantic decorators. These tests ensure compatibility with Python 3.10's union type syntax.
|
Heads up that #813 (opened 2026-06-20) implements the same feature, touching the same two files. Worth the maintainers deciding between them rather than reviewing both in isolation. Having read both, there are two cases this one may not cover. 1. 2. Union members are not validated. Widening #813 handles both by keeping the annotation wide and doing the real check at decoration time: if typing.get_origin(t) is Union: # typing.Union[...]
return all(_is_valid_stream_type(arg) for arg in typing.get_args(t))
_UnionType = getattr(types, "UnionType", None) # X | Y, guarded for 3.9
if _UnionType is not None and isinstance(t, _UnionType):
return all(_is_valid_stream_type(arg) for arg in t.__args__)That also speaks to @elijahbenizzy's question on #607 ("does it actually detect per-instance rather than assume they're all the same?"): recursing into the members is what makes an invalid union fail at decoration time rather than surfacing later during streaming. Not trying to talk down your PR - the smaller diff is appealing and the version guard is careful. But if the two get merged into one, the member validation and the |
eeshsaxena
left a comment
There was a problem hiding this comment.
I tried these tests against an unmodified main and all three pass there, so I don't think they guard the fix.
_validate_and_extract_signature_types_streaming never inspects stream_type at runtime. It checks the three arguments for None and returns stream_type unchanged, so a union already goes through today:
@pydantic_streaming_action(
reads=["count"], writes=["count"],
state_input_type=St, state_output_type=St,
stream_type=ModelA | ModelB, # no error on main
)
def act(state: St): ...stream_type=42 is accepted on main too. I appended the three new tests to tests/integrations/test_burr_pydantic.py at 805131a and got 3 passed. That fits #607, which is about the type hint being rejected by a type checker rather than anything failing at runtime, so the only thing that can catch a regression here is a typing check (mypy over a small example), not pytest.
Separately, the annotation in burr/core/action.py:
stream_type: Union[Type["BaseModel"], Type[dict], Any],Any absorbs the other members of a Union, so this collapses to plain Any and the parameter ends up with no type at all, stream_type=42 included. The version-gated PartialType you added in integrations/pydantic.py is the precise version of this. Importing and using it in action.py would keep the hint meaningful.
One more case: types.UnionType only covers the A | B spelling. typing.Union[ModelA, ModelB] is still valid on 3.10+ and is what people writing 3.9-compatible code use, and it would still be flagged. Worth adding to the alias.
Closes #607
What and Why
The
stream_typeparameter in@streaming_action.pydanticandpydantic_streaming_actioncurrently only accepts a single Pydantic model class ordict. Passing a union type (e.g.ModelA | ModelBusing Python 3.10+ syntax) raises aTypeErrorbecausetypes.UnionTypeis not included in thePartialTypealias.This PR fixes that by expanding
PartialTypeto includetypes.UnionTypeon Python 3.10+ using a version guard, and updating thestream_typeannotation inaction.pyaccordingly.Changes
burr/integrations/pydantic.py: Addedimport sysand a version guard soPartialTypeincludestypes.UnionTypeon Python 3.10+burr/core/action.py: Updated thestream_typeannotation instreaming_action.pydantic()fromUnion[Type["BaseModel"], Type[dict]]toUnion[Type["BaseModel"], Type[dict], Any]tests/integrations/test_burr_pydantic.py: Added 3 regression tests guarded with@pytest.mark.skipif(sys.version_info < (3, 10), ...)How I tested this
Added 3 regression tests in
tests/integrations/test_burr_pydantic.py:test_validate_streaming_signature_accepts_union_stream_type— verifies the internal validation function acceptstypes.UnionTypetest_pydantic_streaming_action_accepts_union_stream_type— verifies@pydantic_streaming_actionworks withstream_type=ModelA | ModelBtest_streaming_action_pydantic_decorator_accepts_union_stream_type— same for the@streaming_action.pydanticdecoratorAll tests are skipped automatically on Python < 3.10.
Notes
PartialTypeis preserved for older versionsAnyannotation inaction.pyis intentionally broad to accommodate future union-like types without requiring another version guardChecklist