Skip to content

Commit bb5f740

Browse files
authored
Remove _utils.Either (#768)
1 parent c33fe4b commit bb5f740

File tree

3 files changed

+31
-77
lines changed

3 files changed

+31
-77
lines changed

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from datetime import datetime, timezone
99
from functools import partial
1010
from types import GenericAlias
11-
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union, cast, overload
11+
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union
1212

1313
from pydantic import BaseModel
1414
from pydantic.json_schema import JsonSchemaValue
@@ -79,61 +79,6 @@ def is_set(t_or_unset: T | Unset) -> TypeGuard[T]:
7979
return t_or_unset is not UNSET
8080

8181

82-
Left = TypeVar('Left')
83-
Right = TypeVar('Right')
84-
85-
86-
class Either(Generic[Left, Right]):
87-
"""Two member Union that records which member was set, this is analogous to Rust enums with two variants.
88-
89-
Usage:
90-
91-
```python
92-
if left_thing := either.left:
93-
use_left(left_thing.value)
94-
else:
95-
use_right(either.right)
96-
```
97-
"""
98-
99-
__slots__ = '_left', '_right'
100-
101-
@overload
102-
def __init__(self, *, left: Left) -> None: ...
103-
104-
@overload
105-
def __init__(self, *, right: Right) -> None: ...
106-
107-
def __init__(self, left: Left | Unset = UNSET, right: Right | Unset = UNSET) -> None:
108-
if left is not UNSET:
109-
assert right is UNSET, '`Either` must receive exactly one argument - `left` or `right`'
110-
self._left: Option[Left] = Some(cast(Left, left))
111-
else:
112-
assert right is not UNSET, '`Either` must receive exactly one argument - `left` or `right`'
113-
self._left = None
114-
self._right = cast(Right, right)
115-
116-
@property
117-
def left(self) -> Option[Left]:
118-
return self._left
119-
120-
@property
121-
def right(self) -> Right:
122-
return self._right
123-
124-
def is_left(self) -> bool:
125-
return self._left is not None
126-
127-
def whichever(self) -> Left | Right:
128-
return self._left.value if self._left is not None else self.right
129-
130-
def __repr__(self):
131-
if left := self._left:
132-
return f'Either(left={left.value!r})'
133-
else:
134-
return f'Either(right={self.right!r})'
135-
136-
13782
@asynccontextmanager
13883
async def group_by_temporal(
13984
aiterable: AsyncIterable[T], soft_max_interval: float | None

pydantic_ai_slim/pydantic_ai/models/test.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@
3434
from .function import _estimate_string_tokens, _estimate_usage # pyright: ignore[reportPrivateUsage]
3535

3636

37+
@dataclass
38+
class _TextResult:
39+
"""A private wrapper class to tag a result that came from the custom_result_text field."""
40+
41+
value: str | None
42+
43+
44+
@dataclass
45+
class _FunctionToolResult:
46+
"""A wrapper class to tag a result that came from the custom_result_args field."""
47+
48+
value: Any | None
49+
50+
3751
@dataclass
3852
class TestModel(Model):
3953
"""A model specifically for testing purposes.
@@ -53,7 +67,7 @@ class TestModel(Model):
5367
call_tools: list[str] | Literal['all'] = 'all'
5468
"""List of tools to call. If `'all'`, all tools will be called."""
5569
custom_result_text: str | None = None
56-
"""If set, this text is return as the final result."""
70+
"""If set, this text is returned as the final result."""
5771
custom_result_args: Any | None = None
5872
"""If set, these args will be passed to the result tool."""
5973
seed: int = 0
@@ -95,21 +109,21 @@ async def agent_model(
95109
if self.custom_result_text is not None:
96110
assert allow_text_result, 'Plain response not allowed, but `custom_result_text` is set.'
97111
assert self.custom_result_args is None, 'Cannot set both `custom_result_text` and `custom_result_args`.'
98-
result: _utils.Either[str | None, Any | None] = _utils.Either(left=self.custom_result_text)
112+
result: _TextResult | _FunctionToolResult = _TextResult(self.custom_result_text)
99113
elif self.custom_result_args is not None:
100114
assert result_tools is not None, 'No result tools provided, but `custom_result_args` is set.'
101115
result_tool = result_tools[0]
102116

103117
if k := result_tool.outer_typed_dict_key:
104-
result = _utils.Either(right={k: self.custom_result_args})
118+
result = _FunctionToolResult({k: self.custom_result_args})
105119
else:
106-
result = _utils.Either(right=self.custom_result_args)
120+
result = _FunctionToolResult(self.custom_result_args)
107121
elif allow_text_result:
108-
result = _utils.Either(left=None)
122+
result = _TextResult(None)
109123
elif result_tools:
110-
result = _utils.Either(right=None)
124+
result = _FunctionToolResult(None)
111125
else:
112-
result = _utils.Either(left=None)
126+
result = _TextResult(None)
113127

114128
return TestAgentModel(tool_calls, result, result_tools, self.seed)
115129

@@ -126,7 +140,7 @@ class TestAgentModel(AgentModel):
126140

127141
tool_calls: list[tuple[str, ToolDefinition]]
128142
# left means the text is plain text; right means it's a function call
129-
result: _utils.Either[str | None, Any | None]
143+
result: _TextResult | _FunctionToolResult
130144
result_tools: list[ToolDefinition]
131145
seed: int
132146
model_name: str = 'test'
@@ -176,16 +190,18 @@ def _request(self, messages: list[ModelMessage], model_settings: ModelSettings |
176190
[
177191
ToolCallPart.from_raw_args(
178192
tool.name,
179-
self.result.right if self.result.right is not None else self.gen_tool_args(tool),
193+
self.result.value
194+
if isinstance(self.result, _FunctionToolResult) and self.result.value is not None
195+
else self.gen_tool_args(tool),
180196
)
181197
for tool in self.result_tools
182198
if tool.name in new_retry_names
183199
]
184200
)
185201
return ModelResponse(parts=retry_parts, model_name=self.model_name)
186202

187-
if response_text := self.result.left:
188-
if response_text.value is None:
203+
if isinstance(self.result, _TextResult):
204+
if (response_text := self.result.value) is None:
189205
# build up details of tool responses
190206
output: dict[str, Any] = {}
191207
for message in messages:
@@ -200,10 +216,10 @@ def _request(self, messages: list[ModelMessage], model_settings: ModelSettings |
200216
else:
201217
return ModelResponse(parts=[TextPart('success (no tool calls)')], model_name=self.model_name)
202218
else:
203-
return ModelResponse(parts=[TextPart(response_text.value)], model_name=self.model_name)
219+
return ModelResponse(parts=[TextPart(response_text)], model_name=self.model_name)
204220
else:
205221
assert self.result_tools, 'No result tools provided'
206-
custom_result_args = self.result.right
222+
custom_result_args = self.result.value
207223
result_tool = self.result_tools[self.seed % len(self.result_tools)]
208224
if custom_result_args is not None:
209225
return ModelResponse(

tests/test_utils.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from inline_snapshot import snapshot
1010

1111
from pydantic_ai import UserError
12-
from pydantic_ai._utils import UNSET, Either, PeekableAsyncStream, check_object_json_schema, group_by_temporal
12+
from pydantic_ai._utils import UNSET, PeekableAsyncStream, check_object_json_schema, group_by_temporal
1313

1414
from .models.mock_async_stream import MockAsyncStream
1515

@@ -49,13 +49,6 @@ def test_check_object_json_schema():
4949
check_object_json_schema(array_schema)
5050

5151

52-
def test_either():
53-
assert repr(Either[int, int](left=123)) == 'Either(left=123)'
54-
assert Either(left=123).whichever() == 123
55-
assert repr(Either[int, int](right=456)) == 'Either(right=456)'
56-
assert Either(right=456).whichever() == 456
57-
58-
5952
@pytest.mark.parametrize('peek_first', [True, False])
6053
@pytest.mark.anyio
6154
async def test_peekable_async_stream(peek_first: bool):

0 commit comments

Comments
 (0)