google-genai 1.56.0__py3-none-any.whl → 1.58.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/genai/_api_client.py +49 -26
- google/genai/_interactions/__init__.py +3 -0
- google/genai/_interactions/_base_client.py +1 -1
- google/genai/_interactions/_client.py +57 -3
- google/genai/_interactions/_client_adapter.py +48 -0
- google/genai/_interactions/types/__init__.py +6 -0
- google/genai/_interactions/types/audio_content.py +2 -0
- google/genai/_interactions/types/audio_content_param.py +2 -0
- google/genai/_interactions/types/content.py +65 -0
- google/genai/_interactions/types/content_delta.py +10 -2
- google/genai/_interactions/types/content_param.py +63 -0
- google/genai/_interactions/types/content_start.py +5 -46
- google/genai/_interactions/types/content_stop.py +1 -2
- google/genai/_interactions/types/document_content.py +2 -0
- google/genai/_interactions/types/document_content_param.py +2 -0
- google/genai/_interactions/types/error_event.py +1 -2
- google/genai/_interactions/types/file_search_call_content.py +32 -0
- google/genai/_interactions/types/file_search_call_content_param.py +31 -0
- google/genai/_interactions/types/generation_config.py +4 -0
- google/genai/_interactions/types/generation_config_param.py +4 -0
- google/genai/_interactions/types/image_config.py +31 -0
- google/genai/_interactions/types/image_config_param.py +30 -0
- google/genai/_interactions/types/image_content.py +2 -0
- google/genai/_interactions/types/image_content_param.py +2 -0
- google/genai/_interactions/types/interaction.py +6 -52
- google/genai/_interactions/types/interaction_create_params.py +4 -22
- google/genai/_interactions/types/interaction_event.py +1 -2
- google/genai/_interactions/types/interaction_sse_event.py +5 -3
- google/genai/_interactions/types/interaction_status_update.py +1 -2
- google/genai/_interactions/types/model.py +1 -0
- google/genai/_interactions/types/model_param.py +1 -0
- google/genai/_interactions/types/turn.py +3 -44
- google/genai/_interactions/types/turn_param.py +4 -40
- google/genai/_interactions/types/usage.py +1 -1
- google/genai/_interactions/types/usage_param.py +1 -1
- google/genai/_interactions/types/video_content.py +2 -0
- google/genai/_interactions/types/video_content_param.py +2 -0
- google/genai/_live_converters.py +118 -34
- google/genai/_local_tokenizer_loader.py +1 -0
- google/genai/_tokens_converters.py +14 -14
- google/genai/_transformers.py +15 -21
- google/genai/batches.py +27 -22
- google/genai/caches.py +42 -42
- google/genai/chats.py +0 -2
- google/genai/client.py +61 -55
- google/genai/files.py +224 -0
- google/genai/live.py +1 -1
- google/genai/models.py +56 -44
- google/genai/tests/__init__.py +21 -0
- google/genai/tests/afc/__init__.py +21 -0
- google/genai/tests/afc/test_convert_if_exist_pydantic_model.py +309 -0
- google/genai/tests/afc/test_convert_number_values_for_function_call_args.py +63 -0
- google/genai/tests/afc/test_find_afc_incompatible_tool_indexes.py +240 -0
- google/genai/tests/afc/test_generate_content_stream_afc.py +530 -0
- google/genai/tests/afc/test_generate_content_stream_afc_thoughts.py +77 -0
- google/genai/tests/afc/test_get_function_map.py +176 -0
- google/genai/tests/afc/test_get_function_response_parts.py +277 -0
- google/genai/tests/afc/test_get_max_remote_calls_for_afc.py +130 -0
- google/genai/tests/afc/test_invoke_function_from_dict_args.py +241 -0
- google/genai/tests/afc/test_raise_error_for_afc_incompatible_config.py +159 -0
- google/genai/tests/afc/test_should_append_afc_history.py +53 -0
- google/genai/tests/afc/test_should_disable_afc.py +214 -0
- google/genai/tests/batches/__init__.py +17 -0
- google/genai/tests/batches/test_cancel.py +77 -0
- google/genai/tests/batches/test_create.py +78 -0
- google/genai/tests/batches/test_create_with_bigquery.py +113 -0
- google/genai/tests/batches/test_create_with_file.py +82 -0
- google/genai/tests/batches/test_create_with_gcs.py +125 -0
- google/genai/tests/batches/test_create_with_inlined_requests.py +255 -0
- google/genai/tests/batches/test_delete.py +86 -0
- google/genai/tests/batches/test_embedding.py +157 -0
- google/genai/tests/batches/test_get.py +78 -0
- google/genai/tests/batches/test_list.py +79 -0
- google/genai/tests/caches/__init__.py +17 -0
- google/genai/tests/caches/constants.py +29 -0
- google/genai/tests/caches/test_create.py +210 -0
- google/genai/tests/caches/test_create_custom_url.py +105 -0
- google/genai/tests/caches/test_delete.py +54 -0
- google/genai/tests/caches/test_delete_custom_url.py +52 -0
- google/genai/tests/caches/test_get.py +94 -0
- google/genai/tests/caches/test_get_custom_url.py +52 -0
- google/genai/tests/caches/test_list.py +68 -0
- google/genai/tests/caches/test_update.py +70 -0
- google/genai/tests/caches/test_update_custom_url.py +58 -0
- google/genai/tests/chats/__init__.py +1 -0
- google/genai/tests/chats/test_get_history.py +598 -0
- google/genai/tests/chats/test_send_message.py +844 -0
- google/genai/tests/chats/test_validate_response.py +90 -0
- google/genai/tests/client/__init__.py +17 -0
- google/genai/tests/client/test_async_stream.py +427 -0
- google/genai/tests/client/test_client_close.py +197 -0
- google/genai/tests/client/test_client_initialization.py +1687 -0
- google/genai/tests/client/test_client_requests.py +221 -0
- google/genai/tests/client/test_custom_client.py +104 -0
- google/genai/tests/client/test_http_options.py +178 -0
- google/genai/tests/client/test_replay_client_equality.py +168 -0
- google/genai/tests/client/test_retries.py +846 -0
- google/genai/tests/client/test_upload_errors.py +136 -0
- google/genai/tests/common/__init__.py +17 -0
- google/genai/tests/common/test_common.py +954 -0
- google/genai/tests/conftest.py +162 -0
- google/genai/tests/documents/__init__.py +17 -0
- google/genai/tests/documents/test_delete.py +51 -0
- google/genai/tests/documents/test_get.py +85 -0
- google/genai/tests/documents/test_list.py +72 -0
- google/genai/tests/errors/__init__.py +1 -0
- google/genai/tests/errors/test_api_error.py +417 -0
- google/genai/tests/file_search_stores/__init__.py +17 -0
- google/genai/tests/file_search_stores/test_create.py +66 -0
- google/genai/tests/file_search_stores/test_delete.py +64 -0
- google/genai/tests/file_search_stores/test_get.py +94 -0
- google/genai/tests/file_search_stores/test_import_file.py +112 -0
- google/genai/tests/file_search_stores/test_list.py +57 -0
- google/genai/tests/file_search_stores/test_upload_to_file_search_store.py +141 -0
- google/genai/tests/files/__init__.py +17 -0
- google/genai/tests/files/test_delete.py +46 -0
- google/genai/tests/files/test_download.py +85 -0
- google/genai/tests/files/test_get.py +46 -0
- google/genai/tests/files/test_list.py +72 -0
- google/genai/tests/files/test_register.py +272 -0
- google/genai/tests/files/test_register_table.py +70 -0
- google/genai/tests/files/test_upload.py +255 -0
- google/genai/tests/imports/test_no_optional_imports.py +28 -0
- google/genai/tests/interactions/test_auth.py +476 -0
- google/genai/tests/interactions/test_integration.py +84 -0
- google/genai/tests/interactions/test_paths.py +105 -0
- google/genai/tests/live/__init__.py +16 -0
- google/genai/tests/live/test_live.py +2143 -0
- google/genai/tests/live/test_live_music.py +362 -0
- google/genai/tests/live/test_live_response.py +163 -0
- google/genai/tests/live/test_send_client_content.py +147 -0
- google/genai/tests/live/test_send_realtime_input.py +268 -0
- google/genai/tests/live/test_send_tool_response.py +222 -0
- google/genai/tests/local_tokenizer/__init__.py +17 -0
- google/genai/tests/local_tokenizer/test_local_tokenizer.py +343 -0
- google/genai/tests/local_tokenizer/test_local_tokenizer_loader.py +235 -0
- google/genai/tests/mcp/__init__.py +17 -0
- google/genai/tests/mcp/test_has_mcp_tool_usage.py +89 -0
- google/genai/tests/mcp/test_mcp_to_gemini_tools.py +191 -0
- google/genai/tests/mcp/test_parse_config_for_mcp_sessions.py +201 -0
- google/genai/tests/mcp/test_parse_config_for_mcp_usage.py +130 -0
- google/genai/tests/mcp/test_set_mcp_usage_header.py +72 -0
- google/genai/tests/models/__init__.py +17 -0
- google/genai/tests/models/constants.py +8 -0
- google/genai/tests/models/test_compute_tokens.py +120 -0
- google/genai/tests/models/test_count_tokens.py +159 -0
- google/genai/tests/models/test_delete.py +107 -0
- google/genai/tests/models/test_edit_image.py +264 -0
- google/genai/tests/models/test_embed_content.py +94 -0
- google/genai/tests/models/test_function_call_streaming.py +442 -0
- google/genai/tests/models/test_generate_content.py +2501 -0
- google/genai/tests/models/test_generate_content_cached_content.py +132 -0
- google/genai/tests/models/test_generate_content_config_zero_value.py +103 -0
- google/genai/tests/models/test_generate_content_from_apikey.py +44 -0
- google/genai/tests/models/test_generate_content_http_options.py +40 -0
- google/genai/tests/models/test_generate_content_image_generation.py +143 -0
- google/genai/tests/models/test_generate_content_mcp.py +343 -0
- google/genai/tests/models/test_generate_content_media_resolution.py +97 -0
- google/genai/tests/models/test_generate_content_model.py +139 -0
- google/genai/tests/models/test_generate_content_part.py +821 -0
- google/genai/tests/models/test_generate_content_thought.py +76 -0
- google/genai/tests/models/test_generate_content_tools.py +1761 -0
- google/genai/tests/models/test_generate_images.py +191 -0
- google/genai/tests/models/test_generate_videos.py +759 -0
- google/genai/tests/models/test_get.py +104 -0
- google/genai/tests/models/test_list.py +233 -0
- google/genai/tests/models/test_recontext_image.py +189 -0
- google/genai/tests/models/test_segment_image.py +148 -0
- google/genai/tests/models/test_update.py +95 -0
- google/genai/tests/models/test_upscale_image.py +157 -0
- google/genai/tests/operations/__init__.py +17 -0
- google/genai/tests/operations/test_get.py +38 -0
- google/genai/tests/public_samples/__init__.py +17 -0
- google/genai/tests/public_samples/test_gemini_text_only.py +34 -0
- google/genai/tests/pytest_helper.py +246 -0
- google/genai/tests/shared/__init__.py +16 -0
- google/genai/tests/shared/batches/__init__.py +14 -0
- google/genai/tests/shared/batches/test_create_delete.py +57 -0
- google/genai/tests/shared/batches/test_create_get_cancel.py +56 -0
- google/genai/tests/shared/batches/test_list.py +40 -0
- google/genai/tests/shared/caches/__init__.py +14 -0
- google/genai/tests/shared/caches/test_create_get_delete.py +67 -0
- google/genai/tests/shared/caches/test_create_update_get.py +71 -0
- google/genai/tests/shared/caches/test_list.py +40 -0
- google/genai/tests/shared/chats/__init__.py +14 -0
- google/genai/tests/shared/chats/test_send_message.py +48 -0
- google/genai/tests/shared/chats/test_send_message_stream.py +50 -0
- google/genai/tests/shared/files/__init__.py +14 -0
- google/genai/tests/shared/files/test_list.py +41 -0
- google/genai/tests/shared/files/test_upload_get_delete.py +54 -0
- google/genai/tests/shared/models/__init__.py +14 -0
- google/genai/tests/shared/models/test_compute_tokens.py +41 -0
- google/genai/tests/shared/models/test_count_tokens.py +40 -0
- google/genai/tests/shared/models/test_edit_image.py +67 -0
- google/genai/tests/shared/models/test_embed.py +40 -0
- google/genai/tests/shared/models/test_generate_content.py +39 -0
- google/genai/tests/shared/models/test_generate_content_stream.py +54 -0
- google/genai/tests/shared/models/test_generate_images.py +40 -0
- google/genai/tests/shared/models/test_generate_videos.py +38 -0
- google/genai/tests/shared/models/test_list.py +37 -0
- google/genai/tests/shared/models/test_recontext_image.py +55 -0
- google/genai/tests/shared/models/test_segment_image.py +52 -0
- google/genai/tests/shared/models/test_upscale_image.py +52 -0
- google/genai/tests/shared/tunings/__init__.py +16 -0
- google/genai/tests/shared/tunings/test_create.py +46 -0
- google/genai/tests/shared/tunings/test_create_get_cancel.py +56 -0
- google/genai/tests/shared/tunings/test_list.py +39 -0
- google/genai/tests/tokens/__init__.py +16 -0
- google/genai/tests/tokens/test_create.py +154 -0
- google/genai/tests/transformers/__init__.py +17 -0
- google/genai/tests/transformers/test_blobs.py +84 -0
- google/genai/tests/transformers/test_bytes.py +15 -0
- google/genai/tests/transformers/test_duck_type.py +96 -0
- google/genai/tests/transformers/test_function_responses.py +72 -0
- google/genai/tests/transformers/test_schema.py +653 -0
- google/genai/tests/transformers/test_t_batch.py +286 -0
- google/genai/tests/transformers/test_t_content.py +160 -0
- google/genai/tests/transformers/test_t_contents.py +398 -0
- google/genai/tests/transformers/test_t_part.py +85 -0
- google/genai/tests/transformers/test_t_parts.py +87 -0
- google/genai/tests/transformers/test_t_tool.py +157 -0
- google/genai/tests/transformers/test_t_tools.py +195 -0
- google/genai/tests/tunings/__init__.py +16 -0
- google/genai/tests/tunings/test_cancel.py +39 -0
- google/genai/tests/tunings/test_end_to_end.py +106 -0
- google/genai/tests/tunings/test_get.py +67 -0
- google/genai/tests/tunings/test_list.py +75 -0
- google/genai/tests/tunings/test_tune.py +268 -0
- google/genai/tests/types/__init__.py +16 -0
- google/genai/tests/types/test_bytes_internal.py +271 -0
- google/genai/tests/types/test_bytes_type.py +152 -0
- google/genai/tests/types/test_future.py +101 -0
- google/genai/tests/types/test_optional_types.py +36 -0
- google/genai/tests/types/test_part_type.py +616 -0
- google/genai/tests/types/test_schema_from_json_schema.py +417 -0
- google/genai/tests/types/test_schema_json_schema.py +468 -0
- google/genai/tests/types/test_types.py +2903 -0
- google/genai/types.py +631 -488
- google/genai/version.py +1 -1
- {google_genai-1.56.0.dist-info → google_genai-1.58.0.dist-info}/METADATA +6 -11
- google_genai-1.58.0.dist-info/RECORD +358 -0
- google_genai-1.56.0.dist-info/RECORD +0 -162
- /google/genai/{_interactions/py.typed → tests/interactions/__init__.py} +0 -0
- {google_genai-1.56.0.dist-info → google_genai-1.58.0.dist-info}/WHEEL +0 -0
- {google_genai-1.56.0.dist-info → google_genai-1.58.0.dist-info}/licenses/LICENSE +0 -0
- {google_genai-1.56.0.dist-info → google_genai-1.58.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
"""Tests tools in the _common module."""
|
|
18
|
+
|
|
19
|
+
from enum import Enum
|
|
20
|
+
import inspect
|
|
21
|
+
import logging
|
|
22
|
+
import textwrap
|
|
23
|
+
import typing
|
|
24
|
+
from typing import List, Optional
|
|
25
|
+
import warnings
|
|
26
|
+
|
|
27
|
+
import pydantic
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from ... import _common
|
|
31
|
+
from ... import types
|
|
32
|
+
from ... import errors
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_warn_once():
|
|
36
|
+
@_common.experimental_warning('Warning!')
|
|
37
|
+
def func():
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
with warnings.catch_warnings(record=True) as w:
|
|
41
|
+
func()
|
|
42
|
+
func()
|
|
43
|
+
|
|
44
|
+
assert len(w) == 1
|
|
45
|
+
assert w[0].category == errors.ExperimentalWarning
|
|
46
|
+
|
|
47
|
+
def test_warn_at_call_line():
|
|
48
|
+
@_common.experimental_warning('Warning!')
|
|
49
|
+
def func():
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
with warnings.catch_warnings(record=True) as captured_warnings:
|
|
53
|
+
call_line = inspect.currentframe().f_lineno + 1
|
|
54
|
+
func()
|
|
55
|
+
|
|
56
|
+
assert captured_warnings[0].lineno == call_line
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_is_struct_type():
|
|
60
|
+
assert _common._is_struct_type(list[dict[str, typing.Any]])
|
|
61
|
+
assert _common._is_struct_type(typing.List[typing.Dict[str, typing.Any]])
|
|
62
|
+
assert not _common._is_struct_type(list[dict[str, int]])
|
|
63
|
+
assert not _common._is_struct_type(list[dict[int, typing.Any]])
|
|
64
|
+
assert not _common._is_struct_type(list[str])
|
|
65
|
+
assert not _common._is_struct_type(dict[str, typing.Any])
|
|
66
|
+
assert not _common._is_struct_type(typing.List[typing.Dict[str, int]])
|
|
67
|
+
assert not _common._is_struct_type(typing.List[typing.Dict[int, typing.Any]])
|
|
68
|
+
assert not _common._is_struct_type(typing.List[str])
|
|
69
|
+
assert not _common._is_struct_type(typing.Dict[str, typing.Any])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize(
|
|
74
|
+
"test_id, initial_target, update_dict, expected_target",
|
|
75
|
+
[
|
|
76
|
+
(
|
|
77
|
+
"simple_update",
|
|
78
|
+
{"a": 1, "b": 2},
|
|
79
|
+
{"b": 3, "c": 4},
|
|
80
|
+
{"a": 1, "b": 3, "c": 4},
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
"nested_update",
|
|
84
|
+
{"a": 1, "b": {"x": 10, "y": 20}},
|
|
85
|
+
{"b": {"y": 30, "z": 40}, "c": 3},
|
|
86
|
+
{"a": 1, "b": {"x": 10, "y": 30, "z": 40}, "c": 3},
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
"add_new_nested_dict",
|
|
90
|
+
{"a": 1},
|
|
91
|
+
{"b": {"x": 10, "y": 20}},
|
|
92
|
+
{"a": 1, "b": {"x": 10, "y": 20}},
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"empty_target",
|
|
96
|
+
{},
|
|
97
|
+
{"a": 1, "b": {"x": 10}},
|
|
98
|
+
{"a": 1, "b": {"x": 10}},
|
|
99
|
+
),
|
|
100
|
+
(
|
|
101
|
+
"empty_update",
|
|
102
|
+
{"a": 1, "b": {"x": 10}},
|
|
103
|
+
{},
|
|
104
|
+
{"a": 1, "b": {"x": 10}},
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"overwrite_non_dict_with_dict",
|
|
108
|
+
{"a": 1, "b": 2},
|
|
109
|
+
{"b": {"x": 10}},
|
|
110
|
+
{"a": 1, "b": {"x": 10}},
|
|
111
|
+
),
|
|
112
|
+
(
|
|
113
|
+
"overwrite_dict_with_non_dict",
|
|
114
|
+
{"a": 1, "b": {"x": 10}},
|
|
115
|
+
{"b": 2},
|
|
116
|
+
{"a": 1, "b": 2},
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"deeper_nesting",
|
|
120
|
+
{"a": {"b": {"c": 1, "d": 2}, "e": 3}},
|
|
121
|
+
{"a": {"b": {"d": 4, "f": 5}, "g": 6}, "h": 7},
|
|
122
|
+
{"a": {"b": {"c": 1, "d": 4, "f": 5}, "e": 3, "g": 6}, "h": 7},
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
"different_value_types",
|
|
126
|
+
{"key1": "string_val", "key2": {"nested_int": 100}},
|
|
127
|
+
{"key1": 123, "key2": {"nested_list": [1, 2, 3]}, "key3": True},
|
|
128
|
+
{
|
|
129
|
+
"key1": 123,
|
|
130
|
+
"key2": {"nested_int": 100, "nested_list": [1, 2, 3]},
|
|
131
|
+
"key3": True,
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
(
|
|
135
|
+
"update_with_empty_nested_dict", # Existing nested dict in target should not be cleared
|
|
136
|
+
{"a": {"b": 1}},
|
|
137
|
+
{"a": {}},
|
|
138
|
+
{"a": {"b": 1}},
|
|
139
|
+
),
|
|
140
|
+
(
|
|
141
|
+
"target_with_empty_nested_dict",
|
|
142
|
+
{"a": {}},
|
|
143
|
+
{"a": {"b": 1}},
|
|
144
|
+
{"a": {"b": 1}},
|
|
145
|
+
),
|
|
146
|
+
(
|
|
147
|
+
"key_case_alignment_check",
|
|
148
|
+
{"first_name": "John", "contact_info": {"email_address": "john@example.com"}},
|
|
149
|
+
{"firstName": "Jane", "contact_info": {"email_address": "jane@example.com", "phone_number": "123"}},
|
|
150
|
+
{"first_name": "Jane", "contact_info": {"email_address": "jane@example.com", "phone_number": "123"}},
|
|
151
|
+
)
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
def test_recursive_dict_update(
|
|
155
|
+
test_id: str, initial_target: dict, update_dict: dict, expected_target: dict
|
|
156
|
+
):
|
|
157
|
+
_common.recursive_dict_update(initial_target, update_dict)
|
|
158
|
+
assert initial_target == expected_target
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize(
|
|
162
|
+
"test_id, initial_target, update_dict, expected_target, expect_warning, expected_log_message_part",
|
|
163
|
+
[
|
|
164
|
+
(
|
|
165
|
+
"type_match_int",
|
|
166
|
+
{"a": 1},
|
|
167
|
+
{"a": 2},
|
|
168
|
+
{"a": 2},
|
|
169
|
+
False,
|
|
170
|
+
"",
|
|
171
|
+
),
|
|
172
|
+
(
|
|
173
|
+
"type_match_dict",
|
|
174
|
+
{"a": {"b": 1}},
|
|
175
|
+
{"a": {"b": 2}},
|
|
176
|
+
{"a": {"b": 2}},
|
|
177
|
+
False,
|
|
178
|
+
"",
|
|
179
|
+
),
|
|
180
|
+
(
|
|
181
|
+
"type_mismatch_int_to_str",
|
|
182
|
+
{"a": 1},
|
|
183
|
+
{"a": "hello"},
|
|
184
|
+
{"a": "hello"},
|
|
185
|
+
True,
|
|
186
|
+
"Type mismatch for key 'a'. Existing type: <class 'int'>, new type: <class 'str'>. Overwriting.",
|
|
187
|
+
),
|
|
188
|
+
(
|
|
189
|
+
"type_mismatch_dict_to_int",
|
|
190
|
+
{"a": {"b": 1}},
|
|
191
|
+
{"a": 100},
|
|
192
|
+
{"a": 100},
|
|
193
|
+
True,
|
|
194
|
+
"Type mismatch for key 'a'. Existing type: <class 'dict'>, new type: <class 'int'>. Overwriting.",
|
|
195
|
+
),
|
|
196
|
+
(
|
|
197
|
+
"type_mismatch_int_to_dict",
|
|
198
|
+
{"a": 100},
|
|
199
|
+
{"a": {"b": 1}},
|
|
200
|
+
{"a": {"b": 1}},
|
|
201
|
+
True,
|
|
202
|
+
"Type mismatch for key 'a'. Existing type: <class 'int'>, new type: <class 'dict'>. Overwriting.",
|
|
203
|
+
),
|
|
204
|
+
("add_new_key", {"a": 1}, {"b": "new"}, {"a": 1, "b": "new"}, False, ""),
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
def test_recursive_dict_update_type_warnings(test_id, initial_target, update_dict, expected_target, expect_warning, expected_log_message_part, caplog):
|
|
208
|
+
_common.recursive_dict_update(initial_target, update_dict)
|
|
209
|
+
assert initial_target == expected_target
|
|
210
|
+
if expect_warning:
|
|
211
|
+
assert len(caplog.records) == 1
|
|
212
|
+
assert caplog.records[0].levelname == "WARNING"
|
|
213
|
+
assert expected_log_message_part in caplog.records[0].message
|
|
214
|
+
else:
|
|
215
|
+
for record in caplog.records:
|
|
216
|
+
if record.levelname == "WARNING" and expected_log_message_part in record.message:
|
|
217
|
+
pytest.fail(f"Unexpected warning logged for {test_id}: {record.message}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.mark.parametrize(
|
|
221
|
+
"test_id, target_dict, update_dict, expected_aligned_dict",
|
|
222
|
+
[
|
|
223
|
+
(
|
|
224
|
+
"simple_snake_to_camel",
|
|
225
|
+
{"first_name": "John", "last_name": "Doe"},
|
|
226
|
+
{"firstName": "Jane", "lastName": "Doe"},
|
|
227
|
+
{"first_name": "Jane", "last_name": "Doe"},
|
|
228
|
+
),
|
|
229
|
+
(
|
|
230
|
+
"simple_camel_to_snake",
|
|
231
|
+
{"firstName": "John", "lastName": "Doe"},
|
|
232
|
+
{"first_name": "Jane", "last_name": "Doe"},
|
|
233
|
+
{"firstName": "Jane", "lastName": "Doe"},
|
|
234
|
+
),
|
|
235
|
+
(
|
|
236
|
+
"nested_dict_alignment",
|
|
237
|
+
{"user_info": {"contact_details": {"email_address": ""}}},
|
|
238
|
+
{"userInfo": {"contactDetails": {"emailAddress": "test@example.com"}}},
|
|
239
|
+
{"user_info": {"contact_details": {"email_address": "test@example.com"}}},
|
|
240
|
+
),
|
|
241
|
+
(
|
|
242
|
+
"list_of_dicts_alignment",
|
|
243
|
+
{"users_list": [{"user_id": 0, "user_name": ""}]},
|
|
244
|
+
{"usersList": [{"userId": 1, "userName": "Alice"}]},
|
|
245
|
+
{"users_list": [{"userId": 1, "userName": "Alice"}]},
|
|
246
|
+
),
|
|
247
|
+
(
|
|
248
|
+
"list_of_dicts_alignment_mixed_case_in_update",
|
|
249
|
+
{"users_list": [{"user_id": 0, "user_name": ""}]},
|
|
250
|
+
{"usersList": [{"user_id": 1, "UserName": "Alice"}]},
|
|
251
|
+
{"users_list": [{"user_id": 1, "UserName": "Alice"}]},
|
|
252
|
+
),
|
|
253
|
+
(
|
|
254
|
+
"list_of_dicts_different_lengths_update_longer",
|
|
255
|
+
{"items_data": [{"item_id": 0}]},
|
|
256
|
+
{"itemsData": [{"itemId": 1}, {"item_id": 2, "itemName": "Extra"}]},
|
|
257
|
+
{"items_data": [{"itemId": 1}, {"item_id": 2, "itemName": "Extra"}]},
|
|
258
|
+
),
|
|
259
|
+
(
|
|
260
|
+
"list_of_dicts_different_lengths_target_longer",
|
|
261
|
+
{"items_data": [{"item_id": 0, "item_name": ""}, {"item_id": 1}]},
|
|
262
|
+
{"itemsData": [{"itemId": 10}]},
|
|
263
|
+
{"items_data": [{"itemId": 10}]},
|
|
264
|
+
),
|
|
265
|
+
(
|
|
266
|
+
"no_matching_keys_preserves_update_case",
|
|
267
|
+
{"key_one": 1},
|
|
268
|
+
{"KEY_TWO": 2, "keyThree": 3},
|
|
269
|
+
{"KEY_TWO": 2, "keyThree": 3},
|
|
270
|
+
),
|
|
271
|
+
(
|
|
272
|
+
"mixed_match_and_no_match",
|
|
273
|
+
{"first_name": "John", "age_years": 30},
|
|
274
|
+
{"firstName": "Jane", "AGE_YEARS": 28, "occupation_title": "Engineer"},
|
|
275
|
+
{"first_name": "Jane", "age_years": 28, "occupation_title": "Engineer"},
|
|
276
|
+
),
|
|
277
|
+
(
|
|
278
|
+
"empty_target_dict",
|
|
279
|
+
{},
|
|
280
|
+
{"new_key": "new_value", "anotherKey": "anotherValue"},
|
|
281
|
+
{"new_key": "new_value", "anotherKey": "anotherValue"},
|
|
282
|
+
),
|
|
283
|
+
(
|
|
284
|
+
"empty_update_dict",
|
|
285
|
+
{"existing_key": "value"},
|
|
286
|
+
{},
|
|
287
|
+
{},
|
|
288
|
+
),
|
|
289
|
+
(
|
|
290
|
+
"target_has_non_dict_value_for_nested_key",
|
|
291
|
+
{"config_settings": 123},
|
|
292
|
+
{"configSettings": {"themeName": "dark"}},
|
|
293
|
+
{"config_settings": {"themeName": "dark"}}, # Overwrites as per recursive_dict_update logic
|
|
294
|
+
),
|
|
295
|
+
(
|
|
296
|
+
"update_has_non_dict_value_for_nested_key",
|
|
297
|
+
{"config_settings": {"theme_name": "light"}},
|
|
298
|
+
{"configSettings": "dark_theme_string"},
|
|
299
|
+
{"config_settings": "dark_theme_string"}, # Overwrites
|
|
300
|
+
),
|
|
301
|
+
(
|
|
302
|
+
"deeply_nested_with_lists",
|
|
303
|
+
{"level_one": {"list_items": [{"item_name": "", "item_value": 0}]}},
|
|
304
|
+
{"levelOne": {"listItems": [{"itemName": "Test", "itemValue": 100}, {"itemName": "Test2", "itemValue": 200}]}},
|
|
305
|
+
{"level_one": {"list_items": [{"itemName": "Test", "itemValue": 100}, {"itemName": "Test2", "itemValue": 200}]}},
|
|
306
|
+
),
|
|
307
|
+
],
|
|
308
|
+
)
|
|
309
|
+
def test_align_key_case(
|
|
310
|
+
test_id: str, target_dict: dict, update_dict: dict, expected_aligned_dict: dict
|
|
311
|
+
):
|
|
312
|
+
aligned_dict = _common.align_key_case(target_dict, update_dict)
|
|
313
|
+
assert aligned_dict == expected_aligned_dict, f"Test failed for: {test_id}"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SimpleModel(_common.BaseModel):
|
|
318
|
+
name: str
|
|
319
|
+
value: int
|
|
320
|
+
is_active: bool = True
|
|
321
|
+
none_field: Optional[str] = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Chain(_common.BaseModel):
|
|
325
|
+
id: int
|
|
326
|
+
child: Optional["Chain"] = None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
Chain.model_rebuild()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class Tree(_common.BaseModel):
|
|
333
|
+
id: int
|
|
334
|
+
children: List["Tree"] = pydantic.Field(default_factory=list)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
Tree.model_rebuild()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ReprFalseModel(_common.BaseModel):
|
|
341
|
+
visible: str
|
|
342
|
+
hidden: str = pydantic.Field("secret", repr=False)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class NonPydantic:
|
|
346
|
+
|
|
347
|
+
def __repr__(self):
|
|
348
|
+
return "NonPydantic(\n attr='value'\n)"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class MyEnum(Enum):
|
|
352
|
+
ONE = 1
|
|
353
|
+
TWO = 2
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class EmptyModel(_common.BaseModel):
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_repr_simple_model_defaults_and_no_none():
|
|
361
|
+
obj = SimpleModel(name="Test Name", value=123)
|
|
362
|
+
expected = textwrap.dedent("""
|
|
363
|
+
SimpleModel(
|
|
364
|
+
is_active=True,
|
|
365
|
+
name='Test Name',
|
|
366
|
+
value=123
|
|
367
|
+
)
|
|
368
|
+
""").strip()
|
|
369
|
+
assert repr(obj) == expected
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_repr_empty_model():
|
|
373
|
+
obj = EmptyModel()
|
|
374
|
+
expected = "EmptyModel()"
|
|
375
|
+
assert repr(obj) == expected
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_repr_nested_model():
|
|
379
|
+
obj = Chain(id=1, child=Chain(id=2))
|
|
380
|
+
expected = textwrap.dedent("""
|
|
381
|
+
Chain(
|
|
382
|
+
child=Chain(
|
|
383
|
+
id=2
|
|
384
|
+
),
|
|
385
|
+
id=1
|
|
386
|
+
)
|
|
387
|
+
""").strip()
|
|
388
|
+
assert repr(obj) == expected
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_repr_circular_model():
|
|
392
|
+
obj1 = Chain(id=1)
|
|
393
|
+
obj2 = Chain(id=2)
|
|
394
|
+
obj1.child = obj2
|
|
395
|
+
obj2.child = obj1 # Circular reference
|
|
396
|
+
expected = textwrap.dedent("""
|
|
397
|
+
Chain(
|
|
398
|
+
child=Chain(
|
|
399
|
+
child=<... Circular reference ...>,
|
|
400
|
+
id=2
|
|
401
|
+
),
|
|
402
|
+
id=1
|
|
403
|
+
)
|
|
404
|
+
""").strip()
|
|
405
|
+
|
|
406
|
+
assert repr(obj1) == expected
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_repr_circular_list():
|
|
410
|
+
my_list = [1, 2]
|
|
411
|
+
my_list.append(my_list)
|
|
412
|
+
expected = textwrap.dedent("""
|
|
413
|
+
[
|
|
414
|
+
1,
|
|
415
|
+
2,
|
|
416
|
+
<... Circular reference ...>,
|
|
417
|
+
]
|
|
418
|
+
""").strip()
|
|
419
|
+
assert _common._pretty_repr(my_list) == expected
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_repr_circular_dict():
|
|
423
|
+
my_dict = {"a": 1}
|
|
424
|
+
my_dict["self"] = my_dict
|
|
425
|
+
expected = textwrap.dedent("""
|
|
426
|
+
{
|
|
427
|
+
'a': 1,
|
|
428
|
+
'self': <... Circular reference ...>
|
|
429
|
+
}
|
|
430
|
+
""").strip()
|
|
431
|
+
assert _common._pretty_repr(my_dict) == expected
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_repr_max_items():
|
|
435
|
+
lst = list(range(10))
|
|
436
|
+
dct = {i: i for i in range(10)}
|
|
437
|
+
st = set(range(10))
|
|
438
|
+
tpl = tuple(range(10))
|
|
439
|
+
|
|
440
|
+
assert (
|
|
441
|
+
"<... 5 more items ...>" in
|
|
442
|
+
_common._pretty_repr(lst, max_items=5)
|
|
443
|
+
)
|
|
444
|
+
assert (
|
|
445
|
+
"<dict len=10>" in _common._pretty_repr(dct, max_items=5))
|
|
446
|
+
assert (
|
|
447
|
+
"<... 5 more items ...>" in _common._pretty_repr(st, max_items=5)
|
|
448
|
+
)
|
|
449
|
+
assert (
|
|
450
|
+
"<... 5 more items ...>" in _common._pretty_repr(tpl, max_items=5)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_repr_max_len_bytes():
|
|
455
|
+
b_data = b"a" * 100
|
|
456
|
+
assert len(_common._pretty_repr(b_data, max_len=90)) == 90 + 3
|
|
457
|
+
assert repr(b_data) == _common._pretty_repr(b_data, max_len=200)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_repr_max_depth_dict():
|
|
461
|
+
nested = {'a': {'a': {'a': {'a': 'a', 'b': 'b'}}}}
|
|
462
|
+
assert "{<... 2 items at Max depth ...>}" in _common._pretty_repr(nested, depth=3)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_repr_max_depth_list():
|
|
466
|
+
nested = [[[["d", "e", "e", "p"]]]]
|
|
467
|
+
assert "[<... 4 items at Max depth ...>]" in _common._pretty_repr(nested, depth=3)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_repr_collections():
|
|
471
|
+
obj = {
|
|
472
|
+
"set": {3, 1, 2},
|
|
473
|
+
"tuple": (4, 5, 6),
|
|
474
|
+
"dict": {"b": 2, "a": 1},
|
|
475
|
+
"list": [7, 8, 9],
|
|
476
|
+
}
|
|
477
|
+
expected = textwrap.dedent("""
|
|
478
|
+
{
|
|
479
|
+
'dict': {
|
|
480
|
+
'a': 1,
|
|
481
|
+
'b': 2
|
|
482
|
+
},
|
|
483
|
+
'list': [
|
|
484
|
+
7,
|
|
485
|
+
8,
|
|
486
|
+
9,
|
|
487
|
+
],
|
|
488
|
+
'set': {
|
|
489
|
+
1,
|
|
490
|
+
2,
|
|
491
|
+
3,
|
|
492
|
+
},
|
|
493
|
+
'tuple': (
|
|
494
|
+
4,
|
|
495
|
+
5,
|
|
496
|
+
6,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
""").strip()
|
|
500
|
+
assert _common._pretty_repr(obj) == expected
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_tuple_collections():
|
|
504
|
+
obj = {
|
|
505
|
+
"tuple0": (),
|
|
506
|
+
"tuple1": (1,),
|
|
507
|
+
"tuple2": (1, 2),
|
|
508
|
+
}
|
|
509
|
+
expected = textwrap.dedent("""
|
|
510
|
+
{
|
|
511
|
+
'tuple0': (),
|
|
512
|
+
'tuple1': (
|
|
513
|
+
1,
|
|
514
|
+
),
|
|
515
|
+
'tuple2': (
|
|
516
|
+
1,
|
|
517
|
+
2,
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
""").strip()
|
|
521
|
+
assert _common._pretty_repr(obj) == expected
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def test_repr_empty_collections():
|
|
525
|
+
assert _common._pretty_repr([]) == "[]"
|
|
526
|
+
assert _common._pretty_repr({}) == "{}"
|
|
527
|
+
assert (
|
|
528
|
+
_common._pretty_repr(set()) == "set()"
|
|
529
|
+
)
|
|
530
|
+
assert _common._pretty_repr(tuple()) == "()"
|
|
531
|
+
assert (
|
|
532
|
+
_common._pretty_repr({"empty_set": set()}) ==
|
|
533
|
+
textwrap.dedent("""
|
|
534
|
+
{
|
|
535
|
+
'empty_set': set()
|
|
536
|
+
}
|
|
537
|
+
""").strip()
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_repr_strings():
|
|
542
|
+
s1 = "line one"
|
|
543
|
+
exp1 = "'line one'"
|
|
544
|
+
assert _common._pretty_repr(s1) == exp1
|
|
545
|
+
|
|
546
|
+
s2 = 'line one\nline two with """ inside'
|
|
547
|
+
exp2 = '"""line one\nline two with \\"\\"\\" inside"""'
|
|
548
|
+
assert _common._pretty_repr(s2) == exp2
|
|
549
|
+
|
|
550
|
+
s3 = 'A string with """ inside'
|
|
551
|
+
exp3 = '\'A string with """ inside\''
|
|
552
|
+
assert _common._pretty_repr(s3) == exp3
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_repr_repr_false():
|
|
556
|
+
obj = ReprFalseModel(visible="show", hidden="hide")
|
|
557
|
+
result = repr(obj)
|
|
558
|
+
assert "visible='show'" in result
|
|
559
|
+
assert "hidden" not in result
|
|
560
|
+
expected = textwrap.dedent("""
|
|
561
|
+
ReprFalseModel(
|
|
562
|
+
visible='show'
|
|
563
|
+
)
|
|
564
|
+
""").strip()
|
|
565
|
+
assert result == expected
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_repr_none_fields():
|
|
569
|
+
obj = SimpleModel(name="Only Name", value=0, none_field=None)
|
|
570
|
+
result = repr(obj)
|
|
571
|
+
assert "none_field" not in result
|
|
572
|
+
expected = textwrap.dedent("""
|
|
573
|
+
SimpleModel(
|
|
574
|
+
is_active=True,
|
|
575
|
+
name='Only Name',
|
|
576
|
+
value=0
|
|
577
|
+
)
|
|
578
|
+
""").strip()
|
|
579
|
+
assert result == expected
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def test_repr_other_types():
|
|
583
|
+
np = NonPydantic()
|
|
584
|
+
en = MyEnum.TWO
|
|
585
|
+
obj = {"np": np, "en": en}
|
|
586
|
+
expected = textwrap.dedent("""
|
|
587
|
+
{
|
|
588
|
+
'en': <MyEnum.TWO: 2>,
|
|
589
|
+
'np': NonPydantic(
|
|
590
|
+
attr='value'
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
""").strip()
|
|
594
|
+
assert _common._pretty_repr(obj) == expected
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def test_repr_indent_delta():
|
|
598
|
+
obj = SimpleModel(name="Indent Test", value=1)
|
|
599
|
+
expected = textwrap.dedent("""
|
|
600
|
+
SimpleModel(
|
|
601
|
+
is_active=True,
|
|
602
|
+
name='Indent Test',
|
|
603
|
+
value=1
|
|
604
|
+
)
|
|
605
|
+
""").strip()
|
|
606
|
+
assert _common._pretty_repr(obj, indent_delta=4) == expected
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def test_repr_complex_object():
|
|
610
|
+
obj = types.GenerateContentResponse(
|
|
611
|
+
automatic_function_calling_history=[],
|
|
612
|
+
candidates=[
|
|
613
|
+
types.Candidate(
|
|
614
|
+
content=types.Content(
|
|
615
|
+
parts=[
|
|
616
|
+
types.Part(
|
|
617
|
+
text="""There isn't a single "best" LLM, as the ideal choice highly depends on your specific needs, use case, budget, and priorities. The field is evolving incredibly fast, with new models and improvements being released constantly.
|
|
618
|
+
|
|
619
|
+
However, we can talk about the **leading contenders** and what they are generally known for:..."""
|
|
620
|
+
)
|
|
621
|
+
],
|
|
622
|
+
role="model"
|
|
623
|
+
),
|
|
624
|
+
finish_reason=types.FinishReason.STOP,
|
|
625
|
+
index=0
|
|
626
|
+
)
|
|
627
|
+
],
|
|
628
|
+
model_version='models/gemini-2.5-flash-preview-05-20',
|
|
629
|
+
usage_metadata=types.GenerateContentResponseUsageMetadata(
|
|
630
|
+
candidates_token_count=1086,
|
|
631
|
+
prompt_token_count=7,
|
|
632
|
+
prompt_tokens_details=[
|
|
633
|
+
types.ModalityTokenCount(
|
|
634
|
+
modality=types.MediaModality.TEXT,
|
|
635
|
+
token_count=7
|
|
636
|
+
)
|
|
637
|
+
],
|
|
638
|
+
thoughts_token_count=860,
|
|
639
|
+
total_token_count=1953
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
expected = textwrap.dedent("""
|
|
644
|
+
GenerateContentResponse(
|
|
645
|
+
automatic_function_calling_history=[],
|
|
646
|
+
candidates=[
|
|
647
|
+
Candidate(
|
|
648
|
+
content=Content(
|
|
649
|
+
parts=[
|
|
650
|
+
Part(
|
|
651
|
+
text=\"\"\"There isn't a single "best" LLM, as the ideal choice highly depends on your specific needs, use case, budget, and priorities. The field is evolving incredibly fast, with new models and improvements being released constantly.
|
|
652
|
+
|
|
653
|
+
However, we can talk about the **leading contenders** and what they are generally known for:...\"\"\"
|
|
654
|
+
),
|
|
655
|
+
],
|
|
656
|
+
role='model'
|
|
657
|
+
),
|
|
658
|
+
finish_reason=<FinishReason.STOP: 'STOP'>,
|
|
659
|
+
index=0
|
|
660
|
+
),
|
|
661
|
+
],
|
|
662
|
+
model_version='models/gemini-2.5-flash-preview-05-20',
|
|
663
|
+
usage_metadata=GenerateContentResponseUsageMetadata(
|
|
664
|
+
candidates_token_count=1086,
|
|
665
|
+
prompt_token_count=7,
|
|
666
|
+
prompt_tokens_details=[
|
|
667
|
+
ModalityTokenCount(
|
|
668
|
+
modality=<MediaModality.TEXT: 'TEXT'>,
|
|
669
|
+
token_count=7
|
|
670
|
+
),
|
|
671
|
+
],
|
|
672
|
+
thoughts_token_count=860,
|
|
673
|
+
total_token_count=1953
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
""").strip()
|
|
677
|
+
assert repr(obj) == expected
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def test_move_value_by_path():
|
|
681
|
+
"""Test move_value_by_path function with array wildcard notation."""
|
|
682
|
+
data = {
|
|
683
|
+
"requests": [
|
|
684
|
+
{
|
|
685
|
+
"request": {
|
|
686
|
+
"content": {
|
|
687
|
+
"parts": [
|
|
688
|
+
{
|
|
689
|
+
"text": "1"
|
|
690
|
+
}
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
"outputDimensionality": 64
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
"request": {
|
|
698
|
+
"content": {
|
|
699
|
+
"parts": [
|
|
700
|
+
{
|
|
701
|
+
"text": "2"
|
|
702
|
+
}
|
|
703
|
+
]
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
"outputDimensionality": 64
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"request": {
|
|
710
|
+
"content": {
|
|
711
|
+
"parts": [
|
|
712
|
+
{
|
|
713
|
+
"text": "3"
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
"outputDimensionality": 64
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
paths = {'requests[].*': 'requests[].request.*'}
|
|
724
|
+
_common.move_value_by_path(data, paths)
|
|
725
|
+
|
|
726
|
+
expected = {
|
|
727
|
+
"requests": [
|
|
728
|
+
{
|
|
729
|
+
"request": {
|
|
730
|
+
"content": {
|
|
731
|
+
"parts": [
|
|
732
|
+
{
|
|
733
|
+
"text": "1"
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
},
|
|
737
|
+
"outputDimensionality": 64
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"request": {
|
|
742
|
+
"content": {
|
|
743
|
+
"parts": [
|
|
744
|
+
{
|
|
745
|
+
"text": "2"
|
|
746
|
+
}
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
"outputDimensionality": 64
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"request": {
|
|
754
|
+
"content": {
|
|
755
|
+
"parts": [
|
|
756
|
+
{
|
|
757
|
+
"text": "3"
|
|
758
|
+
}
|
|
759
|
+
]
|
|
760
|
+
},
|
|
761
|
+
"outputDimensionality": 64
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
assert data == expected
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_check_field_type_mismatches_no_warning_for_correct_types(caplog):
|
|
771
|
+
"""Test that no warning is logged when types match."""
|
|
772
|
+
|
|
773
|
+
class ModelA(_common.BaseModel):
|
|
774
|
+
value: int
|
|
775
|
+
|
|
776
|
+
class TestModel(_common.BaseModel):
|
|
777
|
+
model_a: ModelA
|
|
778
|
+
|
|
779
|
+
# Should not warn - dict will be converted to ModelA by Pydantic
|
|
780
|
+
data = {"model_a": {"value": 123}}
|
|
781
|
+
|
|
782
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
783
|
+
result = TestModel.model_validate(data)
|
|
784
|
+
|
|
785
|
+
assert result.model_a.value == 123
|
|
786
|
+
assert len(caplog.records) == 0
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_check_field_type_mismatches_warns_on_pydantic_type_mismatch(caplog):
|
|
790
|
+
"""Test that warning is logged when Pydantic model types mismatch."""
|
|
791
|
+
|
|
792
|
+
class ModelA(_common.BaseModel):
|
|
793
|
+
value: int
|
|
794
|
+
|
|
795
|
+
class ModelB(_common.BaseModel):
|
|
796
|
+
value: str
|
|
797
|
+
|
|
798
|
+
class TestModel(_common.BaseModel):
|
|
799
|
+
model_field: ModelA
|
|
800
|
+
|
|
801
|
+
# Create an instance of ModelB (wrong type)
|
|
802
|
+
model_b_instance = ModelB(value="test")
|
|
803
|
+
|
|
804
|
+
# Pass the wrong Pydantic model instance
|
|
805
|
+
data = {"model_field": model_b_instance}
|
|
806
|
+
|
|
807
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
808
|
+
TestModel._check_field_type_mismatches(data)
|
|
809
|
+
|
|
810
|
+
assert len(caplog.records) == 1
|
|
811
|
+
assert "Type mismatch in TestModel.model_field" in caplog.records[0].message
|
|
812
|
+
assert "expected ModelA, got ModelB" in caplog.records[0].message
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def test_check_field_type_mismatches_no_warning_for_none_values(caplog):
|
|
816
|
+
"""Test that no warning is logged for None values."""
|
|
817
|
+
|
|
818
|
+
class ModelA(_common.BaseModel):
|
|
819
|
+
value: int
|
|
820
|
+
|
|
821
|
+
class TestModel(_common.BaseModel):
|
|
822
|
+
model_field: Optional[ModelA] = None
|
|
823
|
+
|
|
824
|
+
data = {"model_field": None}
|
|
825
|
+
|
|
826
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
827
|
+
result = TestModel.model_validate(data)
|
|
828
|
+
|
|
829
|
+
assert result.model_field is None
|
|
830
|
+
assert len(caplog.records) == 0
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def test_check_field_type_mismatches_no_warning_for_missing_fields(caplog):
|
|
834
|
+
"""Test that no warning is logged for missing fields."""
|
|
835
|
+
|
|
836
|
+
class ModelA(_common.BaseModel):
|
|
837
|
+
value: int
|
|
838
|
+
|
|
839
|
+
class TestModel(_common.BaseModel):
|
|
840
|
+
model_field: Optional[ModelA] = None
|
|
841
|
+
|
|
842
|
+
data = {}
|
|
843
|
+
|
|
844
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
845
|
+
result = TestModel.model_validate(data)
|
|
846
|
+
|
|
847
|
+
assert result.model_field is None
|
|
848
|
+
assert len(caplog.records) == 0
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def test_check_field_type_mismatches_no_warning_for_primitive_types(caplog):
|
|
852
|
+
"""Test that no warning is logged for primitive type mismatches."""
|
|
853
|
+
|
|
854
|
+
class TestModel(_common.BaseModel):
|
|
855
|
+
int_field: int
|
|
856
|
+
str_field: str
|
|
857
|
+
|
|
858
|
+
# Even though we're passing wrong primitive types, we should not warn
|
|
859
|
+
# (Pydantic will handle validation)
|
|
860
|
+
data = {"int_field": "123", "str_field": "test"}
|
|
861
|
+
|
|
862
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
863
|
+
# This will succeed because Pydantic can coerce "123" to int
|
|
864
|
+
result = TestModel.model_validate(data)
|
|
865
|
+
|
|
866
|
+
assert result.int_field == 123
|
|
867
|
+
assert result.str_field == "test"
|
|
868
|
+
assert len(caplog.records) == 0
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def test_check_field_type_mismatches_handles_optional_unwrapping(caplog):
|
|
872
|
+
"""Test that Optional types are properly unwrapped before checking."""
|
|
873
|
+
|
|
874
|
+
class ModelA(_common.BaseModel):
|
|
875
|
+
value: int
|
|
876
|
+
|
|
877
|
+
class ModelB(_common.BaseModel):
|
|
878
|
+
value: str
|
|
879
|
+
|
|
880
|
+
class TestModel(_common.BaseModel):
|
|
881
|
+
model_field: Optional[ModelA] = None
|
|
882
|
+
|
|
883
|
+
# Pass wrong Pydantic model type
|
|
884
|
+
model_b_instance = ModelB(value="test")
|
|
885
|
+
data = {"model_field": model_b_instance}
|
|
886
|
+
|
|
887
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
888
|
+
TestModel._check_field_type_mismatches(data)
|
|
889
|
+
|
|
890
|
+
assert len(caplog.records) == 1
|
|
891
|
+
assert "expected ModelA, got ModelB" in caplog.records[0].message
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def test_check_field_type_mismatches_no_warning_for_correct_pydantic_instance(caplog):
|
|
895
|
+
"""Test that no warning is logged when correct Pydantic instance is provided."""
|
|
896
|
+
|
|
897
|
+
class ModelA(_common.BaseModel):
|
|
898
|
+
value: int
|
|
899
|
+
|
|
900
|
+
class TestModel(_common.BaseModel):
|
|
901
|
+
model_field: ModelA
|
|
902
|
+
|
|
903
|
+
# Pass correct Pydantic model instance
|
|
904
|
+
model_a_instance = ModelA(value=42)
|
|
905
|
+
data = {"model_field": model_a_instance}
|
|
906
|
+
|
|
907
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
908
|
+
result = TestModel.model_validate(data)
|
|
909
|
+
|
|
910
|
+
assert result.model_field.value == 42
|
|
911
|
+
assert len(caplog.records) == 0
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def test_check_field_type_mismatches_with_multiple_fields(caplog):
|
|
915
|
+
"""Test checking multiple fields with mixed scenarios."""
|
|
916
|
+
|
|
917
|
+
class ModelA(_common.BaseModel):
|
|
918
|
+
value: int
|
|
919
|
+
|
|
920
|
+
class ModelB(_common.BaseModel):
|
|
921
|
+
value: str
|
|
922
|
+
|
|
923
|
+
class TestModel(_common.BaseModel):
|
|
924
|
+
field_a: ModelA
|
|
925
|
+
field_b: Optional[ModelA] = None
|
|
926
|
+
field_c: str
|
|
927
|
+
|
|
928
|
+
model_b_instance = ModelB(value="wrong")
|
|
929
|
+
data = {
|
|
930
|
+
"field_a": model_b_instance, # Wrong type - should warn
|
|
931
|
+
"field_b": None, # None - should not warn
|
|
932
|
+
"field_c": "test", # Primitive - should not warn
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
936
|
+
TestModel._check_field_type_mismatches(data)
|
|
937
|
+
|
|
938
|
+
# Should only warn about field_a
|
|
939
|
+
assert len(caplog.records) == 1
|
|
940
|
+
assert "field_a" in caplog.records[0].message
|
|
941
|
+
assert "expected ModelA, got ModelB" in caplog.records[0].message
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def test_check_field_type_mismatches_generic_type_no_error(caplog):
|
|
945
|
+
"""Test that validation doesn't crash on generic types like list[str]."""
|
|
946
|
+
class TestModel(_common.BaseModel):
|
|
947
|
+
tags: list[str]
|
|
948
|
+
|
|
949
|
+
data = {"tags": ["a", "b"]}
|
|
950
|
+
|
|
951
|
+
with caplog.at_level(logging.WARNING, logger="google_genai._common"):
|
|
952
|
+
TestModel.model_validate(data)
|
|
953
|
+
|
|
954
|
+
assert len(caplog.records) == 0
|