Skip to content

Commit 54d53e5

Browse files
authored
improve messages coverage (#1704)
1 parent 5b2920e commit 54d53e5

File tree

2 files changed

+218
-84
lines changed

2 files changed

+218
-84
lines changed

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 56 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class VideoUrl:
8383
"""Type identifier, this is available on all parts as a discriminator."""
8484

8585
@property
86-
def media_type(self) -> VideoMediaType: # pragma: lax no cover
86+
def media_type(self) -> VideoMediaType:
8787
"""Return the media type of the video, based on the url."""
8888
if self.url.endswith('.mkv'):
8989
return 'video/x-matroska'
@@ -110,7 +110,7 @@ def format(self) -> VideoFormat:
110110
111111
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
112112
"""
113-
return _video_format(self.media_type)
113+
return _video_format_lookup[self.media_type]
114114

115115

116116
@dataclass
@@ -133,6 +133,11 @@ def media_type(self) -> AudioMediaType:
133133
else:
134134
raise ValueError(f'Unknown audio file extension: {self.url}')
135135

136+
@property
137+
def format(self) -> AudioFormat:
138+
"""The file format of the audio file."""
139+
return _audio_format_lookup[self.media_type]
140+
136141

137142
@dataclass
138143
class ImageUrl:
@@ -164,7 +169,7 @@ def format(self) -> ImageFormat:
164169
165170
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
166171
"""
167-
return _image_format(self.media_type)
172+
return _image_format_lookup[self.media_type]
168173

169174

170175
@dataclass
@@ -182,7 +187,7 @@ def media_type(self) -> str:
182187
"""Return the media type of the document, based on the url."""
183188
type_, _ = guess_type(self.url)
184189
if type_ is None:
185-
raise RuntimeError(f'Unknown document file extension: {self.url}')
190+
raise ValueError(f'Unknown document file extension: {self.url}')
186191
return type_
187192

188193
@property
@@ -191,7 +196,11 @@ def format(self) -> DocumentFormat:
191196
192197
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
193198
"""
194-
return _document_format(self.media_type)
199+
media_type = self.media_type
200+
try:
201+
return _document_format_lookup[media_type]
202+
except KeyError as e:
203+
raise ValueError(f'Unknown document media type: {media_type}') from e
195204

196205

197206
@dataclass
@@ -225,93 +234,58 @@ def is_video(self) -> bool:
225234
@property
226235
def is_document(self) -> bool:
227236
"""Return `True` if the media type is a document type."""
228-
return self.media_type in {
229-
'application/pdf',
230-
'text/plain',
231-
'text/csv',
232-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
233-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
234-
'text/html',
235-
'text/markdown',
236-
'application/vnd.ms-excel',
237-
}
237+
return self.media_type in _document_format_lookup
238238

239239
@property
240240
def format(self) -> str:
241241
"""The file format of the binary content."""
242-
if self.is_audio:
243-
if self.media_type == 'audio/mpeg':
244-
return 'mp3'
245-
elif self.media_type == 'audio/wav':
246-
return 'wav'
247-
elif self.is_image:
248-
return _image_format(self.media_type)
249-
elif self.is_document:
250-
return _document_format(self.media_type)
251-
elif self.is_video:
252-
return _video_format(self.media_type)
253-
raise ValueError(f'Unknown media type: {self.media_type}')
242+
try:
243+
if self.is_audio:
244+
return _audio_format_lookup[self.media_type]
245+
elif self.is_image:
246+
return _image_format_lookup[self.media_type]
247+
elif self.is_video:
248+
return _video_format_lookup[self.media_type]
249+
else:
250+
return _document_format_lookup[self.media_type]
251+
except KeyError as e:
252+
raise ValueError(f'Unknown media type: {self.media_type}') from e
254253

255254

256255
UserContent: TypeAlias = 'str | ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent'
257256

258257
# Ideally this would be a Union of types, but Python 3.9 requires it to be a string, and strings don't work with `isinstance``.
259258
MultiModalContentTypes = (ImageUrl, AudioUrl, DocumentUrl, VideoUrl, BinaryContent)
260-
261-
262-
def _document_format(media_type: str) -> DocumentFormat:
263-
if media_type == 'application/pdf':
264-
return 'pdf'
265-
elif media_type == 'text/plain':
266-
return 'txt'
267-
elif media_type == 'text/csv':
268-
return 'csv'
269-
elif media_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
270-
return 'docx'
271-
elif media_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
272-
return 'xlsx'
273-
elif media_type == 'text/html':
274-
return 'html'
275-
elif media_type == 'text/markdown':
276-
return 'md'
277-
elif media_type == 'application/vnd.ms-excel':
278-
return 'xls'
279-
else:
280-
raise ValueError(f'Unknown document media type: {media_type}')
281-
282-
283-
def _image_format(media_type: str) -> ImageFormat:
284-
if media_type == 'image/jpeg':
285-
return 'jpeg'
286-
elif media_type == 'image/png':
287-
return 'png'
288-
elif media_type == 'image/gif':
289-
return 'gif'
290-
elif media_type == 'image/webp':
291-
return 'webp'
292-
else:
293-
raise ValueError(f'Unknown image media type: {media_type}')
294-
295-
296-
def _video_format(media_type: str) -> VideoFormat:
297-
if media_type == 'video/x-matroska':
298-
return 'mkv'
299-
elif media_type == 'video/quicktime':
300-
return 'mov'
301-
elif media_type == 'video/mp4':
302-
return 'mp4'
303-
elif media_type == 'video/webm':
304-
return 'webm'
305-
elif media_type == 'video/x-flv':
306-
return 'flv'
307-
elif media_type == 'video/mpeg':
308-
return 'mpeg'
309-
elif media_type == 'video/x-ms-wmv':
310-
return 'wmv'
311-
elif media_type == 'video/3gpp':
312-
return 'three_gp'
313-
else: # pragma: no cover
314-
raise ValueError(f'Unknown video media type: {media_type}')
259+
_document_format_lookup: dict[str, DocumentFormat] = {
260+
'application/pdf': 'pdf',
261+
'text/plain': 'txt',
262+
'text/csv': 'csv',
263+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
264+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
265+
'text/html': 'html',
266+
'text/markdown': 'md',
267+
'application/vnd.ms-excel': 'xls',
268+
}
269+
_audio_format_lookup: dict[str, AudioFormat] = {
270+
'audio/mpeg': 'mp3',
271+
'audio/wav': 'wav',
272+
}
273+
_image_format_lookup: dict[str, ImageFormat] = {
274+
'image/jpeg': 'jpeg',
275+
'image/png': 'png',
276+
'image/gif': 'gif',
277+
'image/webp': 'webp',
278+
}
279+
_video_format_lookup: dict[str, VideoFormat] = {
280+
'video/x-matroska': 'mkv',
281+
'video/quicktime': 'mov',
282+
'video/mp4': 'mp4',
283+
'video/webm': 'webm',
284+
'video/x-flv': 'flv',
285+
'video/mpeg': 'mpeg',
286+
'video/x-ms-wmv': 'wmv',
287+
'video/3gpp': 'three_gp',
288+
}
315289

316290

317291
@dataclass

tests/test_messages.py

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from pydantic_ai.messages import BinaryContent, DocumentUrl, ImageUrl, VideoUrl
3+
from pydantic_ai.messages import AudioUrl, BinaryContent, DocumentUrl, ImageUrl, VideoUrl
44

55

66
def test_image_url():
@@ -20,7 +20,7 @@ def test_video_url():
2020

2121

2222
def test_document_url():
23-
with pytest.raises(RuntimeError, match='Unknown document file extension: https://example.com/document.potato'):
23+
with pytest.raises(ValueError, match='Unknown document file extension: https://example.com/document.potato'):
2424
document_url = DocumentUrl(url='https://example.com/document.potato')
2525
document_url.media_type
2626

@@ -93,3 +93,163 @@ def test_binary_content_document(media_type: str, format: str):
9393
binary_content = BinaryContent(data=b'Hello, world!', media_type=media_type)
9494
assert binary_content.is_document
9595
assert binary_content.format == format
96+
97+
98+
@pytest.mark.parametrize(
99+
'audio_url,media_type,format',
100+
[
101+
pytest.param(AudioUrl('foobar.mp3'), 'audio/mpeg', 'mp3', id='mp3'),
102+
pytest.param(AudioUrl('foobar.wav'), 'audio/wav', 'wav', id='wav'),
103+
],
104+
)
105+
def test_audio_url(audio_url: AudioUrl, media_type: str, format: str):
106+
assert audio_url.media_type == media_type
107+
assert audio_url.format == format
108+
109+
110+
def test_audio_url_invalid():
111+
with pytest.raises(ValueError, match='Unknown audio file extension: foobar.potato'):
112+
AudioUrl('foobar.potato').media_type
113+
114+
115+
@pytest.mark.parametrize(
116+
'image_url,media_type,format',
117+
[
118+
pytest.param(ImageUrl('foobar.jpg'), 'image/jpeg', 'jpeg', id='jpg'),
119+
pytest.param(ImageUrl('foobar.jpeg'), 'image/jpeg', 'jpeg', id='jpeg'),
120+
pytest.param(ImageUrl('foobar.png'), 'image/png', 'png', id='png'),
121+
pytest.param(ImageUrl('foobar.gif'), 'image/gif', 'gif', id='gif'),
122+
pytest.param(ImageUrl('foobar.webp'), 'image/webp', 'webp', id='webp'),
123+
],
124+
)
125+
def test_image_url_formats(image_url: ImageUrl, media_type: str, format: str):
126+
assert image_url.media_type == media_type
127+
assert image_url.format == format
128+
129+
130+
def test_image_url_invalid():
131+
with pytest.raises(ValueError, match='Unknown image file extension: foobar.potato'):
132+
ImageUrl('foobar.potato').media_type
133+
134+
with pytest.raises(ValueError, match='Unknown image file extension: foobar.potato'):
135+
ImageUrl('foobar.potato').format
136+
137+
138+
@pytest.mark.parametrize(
139+
'document_url,media_type,format',
140+
[
141+
pytest.param(DocumentUrl('foobar.pdf'), 'application/pdf', 'pdf', id='pdf'),
142+
pytest.param(DocumentUrl('foobar.txt'), 'text/plain', 'txt', id='txt'),
143+
pytest.param(DocumentUrl('foobar.csv'), 'text/csv', 'csv', id='csv'),
144+
pytest.param(
145+
DocumentUrl('foobar.docx'),
146+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
147+
'docx',
148+
id='docx',
149+
),
150+
pytest.param(
151+
DocumentUrl('foobar.xlsx'),
152+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
153+
'xlsx',
154+
id='xlsx',
155+
),
156+
pytest.param(DocumentUrl('foobar.html'), 'text/html', 'html', id='html'),
157+
pytest.param(DocumentUrl('foobar.md'), 'text/markdown', 'md', id='md'),
158+
pytest.param(DocumentUrl('foobar.xls'), 'application/vnd.ms-excel', 'xls', id='xls'),
159+
],
160+
)
161+
def test_document_url_formats(document_url: DocumentUrl, media_type: str, format: str):
162+
assert document_url.media_type == media_type
163+
assert document_url.format == format
164+
165+
166+
def test_document_url_invalid():
167+
with pytest.raises(ValueError, match='Unknown document file extension: foobar.potato'):
168+
DocumentUrl('foobar.potato').media_type
169+
170+
with pytest.raises(ValueError, match='Unknown document media type: text/x-python'):
171+
DocumentUrl('foobar.py').format
172+
173+
174+
def test_binary_content_unknown_media_type():
175+
with pytest.raises(ValueError, match='Unknown media type: application/custom'):
176+
binary_content = BinaryContent(data=b'Hello, world!', media_type='application/custom')
177+
binary_content.format
178+
179+
180+
def test_binary_content_is_methods():
181+
# Test that is_X returns False for non-matching media types
182+
audio_content = BinaryContent(data=b'Hello, world!', media_type='audio/wav')
183+
assert audio_content.is_audio is True
184+
assert audio_content.is_image is False
185+
assert audio_content.is_video is False
186+
assert audio_content.is_document is False
187+
assert audio_content.format == 'wav'
188+
189+
audio_content = BinaryContent(data=b'Hello, world!', media_type='audio/wrong')
190+
assert audio_content.is_audio is True
191+
assert audio_content.is_image is False
192+
assert audio_content.is_video is False
193+
assert audio_content.is_document is False
194+
with pytest.raises(ValueError, match='Unknown media type: audio/wrong'):
195+
audio_content.format
196+
197+
audio_content = BinaryContent(data=b'Hello, world!', media_type='image/wrong')
198+
assert audio_content.is_audio is False
199+
assert audio_content.is_image is True
200+
assert audio_content.is_video is False
201+
assert audio_content.is_document is False
202+
with pytest.raises(ValueError, match='Unknown media type: image/wrong'):
203+
audio_content.format
204+
205+
image_content = BinaryContent(data=b'Hello, world!', media_type='image/jpeg')
206+
assert image_content.is_audio is False
207+
assert image_content.is_image is True
208+
assert image_content.is_video is False
209+
assert image_content.is_document is False
210+
assert image_content.format == 'jpeg'
211+
212+
video_content = BinaryContent(data=b'Hello, world!', media_type='video/mp4')
213+
assert video_content.is_audio is False
214+
assert video_content.is_image is False
215+
assert video_content.is_video is True
216+
assert video_content.is_document is False
217+
assert video_content.format == 'mp4'
218+
219+
video_content = BinaryContent(data=b'Hello, world!', media_type='video/wrong')
220+
assert video_content.is_audio is False
221+
assert video_content.is_image is False
222+
assert video_content.is_video is True
223+
assert video_content.is_document is False
224+
with pytest.raises(ValueError, match='Unknown media type: video/wrong'):
225+
video_content.format
226+
227+
document_content = BinaryContent(data=b'Hello, world!', media_type='application/pdf')
228+
assert document_content.is_audio is False
229+
assert document_content.is_image is False
230+
assert document_content.is_video is False
231+
assert document_content.is_document is True
232+
assert document_content.format == 'pdf'
233+
234+
235+
@pytest.mark.parametrize(
236+
'video_url,media_type,format',
237+
[
238+
pytest.param(VideoUrl('foobar.mp4'), 'video/mp4', 'mp4', id='mp4'),
239+
pytest.param(VideoUrl('foobar.mov'), 'video/quicktime', 'mov', id='mov'),
240+
pytest.param(VideoUrl('foobar.mkv'), 'video/x-matroska', 'mkv', id='mkv'),
241+
pytest.param(VideoUrl('foobar.webm'), 'video/webm', 'webm', id='webm'),
242+
pytest.param(VideoUrl('foobar.flv'), 'video/x-flv', 'flv', id='flv'),
243+
pytest.param(VideoUrl('foobar.mpeg'), 'video/mpeg', 'mpeg', id='mpeg'),
244+
pytest.param(VideoUrl('foobar.wmv'), 'video/x-ms-wmv', 'wmv', id='wmv'),
245+
pytest.param(VideoUrl('foobar.three_gp'), 'video/3gpp', 'three_gp', id='three_gp'),
246+
],
247+
)
248+
def test_video_url_formats(video_url: VideoUrl, media_type: str, format: str):
249+
assert video_url.media_type == media_type
250+
assert video_url.format == format
251+
252+
253+
def test_video_url_invalid():
254+
with pytest.raises(ValueError, match='Unknown video file extension: foobar.potato'):
255+
VideoUrl('foobar.potato').media_type

0 commit comments

Comments
 (0)