google-genai 1.55.0__py3-none-any.whl → 1.56.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.
Files changed (241) hide show
  1. google/genai/_interactions/_base_client.py +8 -2
  2. google/genai/_interactions/resources/interactions.py +6 -6
  3. google/genai/_interactions/types/__init__.py +2 -0
  4. google/genai/_interactions/types/audio_content.py +0 -1
  5. google/genai/_interactions/types/audio_content_param.py +0 -1
  6. google/genai/_interactions/types/code_execution_call_content.py +0 -1
  7. google/genai/_interactions/types/code_execution_call_content_param.py +0 -1
  8. google/genai/_interactions/types/code_execution_result_content.py +0 -1
  9. google/genai/_interactions/types/code_execution_result_content_param.py +0 -1
  10. google/genai/_interactions/types/content_delta.py +7 -23
  11. google/genai/_interactions/types/deep_research_agent_config.py +0 -1
  12. google/genai/_interactions/types/deep_research_agent_config_param.py +0 -1
  13. google/genai/_interactions/types/document_content.py +3 -2
  14. google/genai/_interactions/types/document_content_param.py +3 -2
  15. google/genai/{tests/__init__.py → _interactions/types/document_mime_type.py} +5 -3
  16. google/genai/{tests/afc/__init__.py → _interactions/types/document_mime_type_param.py} +8 -4
  17. google/genai/_interactions/types/dynamic_agent_config.py +0 -1
  18. google/genai/_interactions/types/dynamic_agent_config_param.py +0 -1
  19. google/genai/_interactions/types/file_search_result_content.py +0 -1
  20. google/genai/_interactions/types/file_search_result_content_param.py +0 -1
  21. google/genai/_interactions/types/function_call_content.py +0 -1
  22. google/genai/_interactions/types/function_call_content_param.py +0 -1
  23. google/genai/_interactions/types/function_result_content.py +1 -2
  24. google/genai/_interactions/types/function_result_content_param.py +1 -2
  25. google/genai/_interactions/types/google_search_call_content.py +0 -1
  26. google/genai/_interactions/types/google_search_call_content_param.py +0 -1
  27. google/genai/_interactions/types/google_search_result_content.py +0 -1
  28. google/genai/_interactions/types/google_search_result_content_param.py +0 -1
  29. google/genai/_interactions/types/image_content.py +1 -2
  30. google/genai/_interactions/types/image_content_param.py +1 -2
  31. google/genai/_interactions/types/mcp_server_tool_call_content.py +0 -1
  32. google/genai/_interactions/types/mcp_server_tool_call_content_param.py +0 -1
  33. google/genai/_interactions/types/mcp_server_tool_result_content.py +1 -2
  34. google/genai/_interactions/types/mcp_server_tool_result_content_param.py +1 -2
  35. google/genai/_interactions/types/text_content.py +0 -1
  36. google/genai/_interactions/types/text_content_param.py +0 -1
  37. google/genai/_interactions/types/thinking_level.py +1 -1
  38. google/genai/_interactions/types/thought_content.py +0 -1
  39. google/genai/_interactions/types/thought_content_param.py +0 -1
  40. google/genai/_interactions/types/url_context_call_content.py +0 -1
  41. google/genai/_interactions/types/url_context_call_content_param.py +0 -1
  42. google/genai/_interactions/types/url_context_result_content.py +0 -1
  43. google/genai/_interactions/types/url_context_result_content_param.py +0 -1
  44. google/genai/_interactions/types/video_content.py +1 -2
  45. google/genai/_interactions/types/video_content_param.py +1 -2
  46. google/genai/_live_converters.py +2 -30
  47. google/genai/client.py +3 -1
  48. google/genai/models.py +2 -29
  49. google/genai/tunings.py +1 -27
  50. google/genai/types.py +20 -22
  51. google/genai/version.py +1 -1
  52. {google_genai-1.55.0.dist-info → google_genai-1.56.0.dist-info}/METADATA +224 -22
  53. google_genai-1.56.0.dist-info/RECORD +162 -0
  54. google/genai/tests/afc/test_convert_if_exist_pydantic_model.py +0 -309
  55. google/genai/tests/afc/test_convert_number_values_for_function_call_args.py +0 -63
  56. google/genai/tests/afc/test_find_afc_incompatible_tool_indexes.py +0 -240
  57. google/genai/tests/afc/test_generate_content_stream_afc.py +0 -530
  58. google/genai/tests/afc/test_generate_content_stream_afc_thoughts.py +0 -77
  59. google/genai/tests/afc/test_get_function_map.py +0 -176
  60. google/genai/tests/afc/test_get_function_response_parts.py +0 -277
  61. google/genai/tests/afc/test_get_max_remote_calls_for_afc.py +0 -130
  62. google/genai/tests/afc/test_invoke_function_from_dict_args.py +0 -241
  63. google/genai/tests/afc/test_raise_error_for_afc_incompatible_config.py +0 -159
  64. google/genai/tests/afc/test_should_append_afc_history.py +0 -53
  65. google/genai/tests/afc/test_should_disable_afc.py +0 -214
  66. google/genai/tests/batches/__init__.py +0 -17
  67. google/genai/tests/batches/test_cancel.py +0 -77
  68. google/genai/tests/batches/test_create.py +0 -78
  69. google/genai/tests/batches/test_create_with_bigquery.py +0 -113
  70. google/genai/tests/batches/test_create_with_file.py +0 -82
  71. google/genai/tests/batches/test_create_with_gcs.py +0 -125
  72. google/genai/tests/batches/test_create_with_inlined_requests.py +0 -255
  73. google/genai/tests/batches/test_delete.py +0 -86
  74. google/genai/tests/batches/test_embedding.py +0 -157
  75. google/genai/tests/batches/test_get.py +0 -78
  76. google/genai/tests/batches/test_list.py +0 -79
  77. google/genai/tests/caches/__init__.py +0 -17
  78. google/genai/tests/caches/constants.py +0 -29
  79. google/genai/tests/caches/test_create.py +0 -210
  80. google/genai/tests/caches/test_create_custom_url.py +0 -105
  81. google/genai/tests/caches/test_delete.py +0 -54
  82. google/genai/tests/caches/test_delete_custom_url.py +0 -52
  83. google/genai/tests/caches/test_get.py +0 -94
  84. google/genai/tests/caches/test_get_custom_url.py +0 -52
  85. google/genai/tests/caches/test_list.py +0 -68
  86. google/genai/tests/caches/test_update.py +0 -70
  87. google/genai/tests/caches/test_update_custom_url.py +0 -58
  88. google/genai/tests/chats/__init__.py +0 -1
  89. google/genai/tests/chats/test_get_history.py +0 -597
  90. google/genai/tests/chats/test_send_message.py +0 -844
  91. google/genai/tests/chats/test_validate_response.py +0 -90
  92. google/genai/tests/client/__init__.py +0 -17
  93. google/genai/tests/client/test_async_stream.py +0 -427
  94. google/genai/tests/client/test_client_close.py +0 -197
  95. google/genai/tests/client/test_client_initialization.py +0 -1687
  96. google/genai/tests/client/test_client_requests.py +0 -355
  97. google/genai/tests/client/test_custom_client.py +0 -77
  98. google/genai/tests/client/test_http_options.py +0 -178
  99. google/genai/tests/client/test_replay_client_equality.py +0 -168
  100. google/genai/tests/client/test_retries.py +0 -846
  101. google/genai/tests/client/test_upload_errors.py +0 -136
  102. google/genai/tests/common/__init__.py +0 -17
  103. google/genai/tests/common/test_common.py +0 -954
  104. google/genai/tests/conftest.py +0 -162
  105. google/genai/tests/documents/__init__.py +0 -17
  106. google/genai/tests/documents/test_delete.py +0 -51
  107. google/genai/tests/documents/test_get.py +0 -85
  108. google/genai/tests/documents/test_list.py +0 -72
  109. google/genai/tests/errors/__init__.py +0 -1
  110. google/genai/tests/errors/test_api_error.py +0 -417
  111. google/genai/tests/file_search_stores/__init__.py +0 -17
  112. google/genai/tests/file_search_stores/test_create.py +0 -66
  113. google/genai/tests/file_search_stores/test_delete.py +0 -64
  114. google/genai/tests/file_search_stores/test_get.py +0 -94
  115. google/genai/tests/file_search_stores/test_import_file.py +0 -112
  116. google/genai/tests/file_search_stores/test_list.py +0 -57
  117. google/genai/tests/file_search_stores/test_upload_to_file_search_store.py +0 -141
  118. google/genai/tests/files/__init__.py +0 -17
  119. google/genai/tests/files/test_delete.py +0 -46
  120. google/genai/tests/files/test_download.py +0 -85
  121. google/genai/tests/files/test_get.py +0 -46
  122. google/genai/tests/files/test_list.py +0 -72
  123. google/genai/tests/files/test_upload.py +0 -255
  124. google/genai/tests/imports/test_no_optional_imports.py +0 -28
  125. google/genai/tests/interactions/test_integration.py +0 -80
  126. google/genai/tests/live/__init__.py +0 -16
  127. google/genai/tests/live/test_live.py +0 -2177
  128. google/genai/tests/live/test_live_music.py +0 -362
  129. google/genai/tests/live/test_live_response.py +0 -163
  130. google/genai/tests/live/test_send_client_content.py +0 -147
  131. google/genai/tests/live/test_send_realtime_input.py +0 -268
  132. google/genai/tests/live/test_send_tool_response.py +0 -222
  133. google/genai/tests/local_tokenizer/__init__.py +0 -17
  134. google/genai/tests/local_tokenizer/test_local_tokenizer.py +0 -343
  135. google/genai/tests/local_tokenizer/test_local_tokenizer_loader.py +0 -235
  136. google/genai/tests/mcp/__init__.py +0 -17
  137. google/genai/tests/mcp/test_has_mcp_tool_usage.py +0 -89
  138. google/genai/tests/mcp/test_mcp_to_gemini_tools.py +0 -191
  139. google/genai/tests/mcp/test_parse_config_for_mcp_sessions.py +0 -201
  140. google/genai/tests/mcp/test_parse_config_for_mcp_usage.py +0 -130
  141. google/genai/tests/mcp/test_set_mcp_usage_header.py +0 -72
  142. google/genai/tests/models/__init__.py +0 -17
  143. google/genai/tests/models/constants.py +0 -8
  144. google/genai/tests/models/test_compute_tokens.py +0 -120
  145. google/genai/tests/models/test_count_tokens.py +0 -159
  146. google/genai/tests/models/test_delete.py +0 -107
  147. google/genai/tests/models/test_edit_image.py +0 -264
  148. google/genai/tests/models/test_embed_content.py +0 -94
  149. google/genai/tests/models/test_function_call_streaming.py +0 -442
  150. google/genai/tests/models/test_generate_content.py +0 -2502
  151. google/genai/tests/models/test_generate_content_cached_content.py +0 -132
  152. google/genai/tests/models/test_generate_content_config_zero_value.py +0 -103
  153. google/genai/tests/models/test_generate_content_from_apikey.py +0 -44
  154. google/genai/tests/models/test_generate_content_http_options.py +0 -40
  155. google/genai/tests/models/test_generate_content_image_generation.py +0 -143
  156. google/genai/tests/models/test_generate_content_mcp.py +0 -343
  157. google/genai/tests/models/test_generate_content_media_resolution.py +0 -97
  158. google/genai/tests/models/test_generate_content_model.py +0 -139
  159. google/genai/tests/models/test_generate_content_part.py +0 -821
  160. google/genai/tests/models/test_generate_content_thought.py +0 -76
  161. google/genai/tests/models/test_generate_content_tools.py +0 -1761
  162. google/genai/tests/models/test_generate_images.py +0 -191
  163. google/genai/tests/models/test_generate_videos.py +0 -759
  164. google/genai/tests/models/test_get.py +0 -104
  165. google/genai/tests/models/test_list.py +0 -233
  166. google/genai/tests/models/test_recontext_image.py +0 -189
  167. google/genai/tests/models/test_segment_image.py +0 -148
  168. google/genai/tests/models/test_update.py +0 -95
  169. google/genai/tests/models/test_upscale_image.py +0 -157
  170. google/genai/tests/operations/__init__.py +0 -17
  171. google/genai/tests/operations/test_get.py +0 -38
  172. google/genai/tests/public_samples/__init__.py +0 -17
  173. google/genai/tests/public_samples/test_gemini_text_only.py +0 -34
  174. google/genai/tests/pytest_helper.py +0 -229
  175. google/genai/tests/shared/__init__.py +0 -16
  176. google/genai/tests/shared/batches/__init__.py +0 -14
  177. google/genai/tests/shared/batches/test_create_delete.py +0 -57
  178. google/genai/tests/shared/batches/test_create_get_cancel.py +0 -56
  179. google/genai/tests/shared/batches/test_list.py +0 -40
  180. google/genai/tests/shared/caches/__init__.py +0 -14
  181. google/genai/tests/shared/caches/test_create_get_delete.py +0 -67
  182. google/genai/tests/shared/caches/test_create_update_get.py +0 -71
  183. google/genai/tests/shared/caches/test_list.py +0 -40
  184. google/genai/tests/shared/chats/__init__.py +0 -14
  185. google/genai/tests/shared/chats/test_send_message.py +0 -48
  186. google/genai/tests/shared/chats/test_send_message_stream.py +0 -50
  187. google/genai/tests/shared/files/__init__.py +0 -14
  188. google/genai/tests/shared/files/test_list.py +0 -41
  189. google/genai/tests/shared/files/test_upload_get_delete.py +0 -54
  190. google/genai/tests/shared/models/__init__.py +0 -14
  191. google/genai/tests/shared/models/test_compute_tokens.py +0 -41
  192. google/genai/tests/shared/models/test_count_tokens.py +0 -40
  193. google/genai/tests/shared/models/test_edit_image.py +0 -67
  194. google/genai/tests/shared/models/test_embed.py +0 -40
  195. google/genai/tests/shared/models/test_generate_content.py +0 -39
  196. google/genai/tests/shared/models/test_generate_content_stream.py +0 -54
  197. google/genai/tests/shared/models/test_generate_images.py +0 -40
  198. google/genai/tests/shared/models/test_generate_videos.py +0 -38
  199. google/genai/tests/shared/models/test_list.py +0 -37
  200. google/genai/tests/shared/models/test_recontext_image.py +0 -55
  201. google/genai/tests/shared/models/test_segment_image.py +0 -52
  202. google/genai/tests/shared/models/test_upscale_image.py +0 -52
  203. google/genai/tests/shared/tunings/__init__.py +0 -16
  204. google/genai/tests/shared/tunings/test_create.py +0 -46
  205. google/genai/tests/shared/tunings/test_create_get_cancel.py +0 -56
  206. google/genai/tests/shared/tunings/test_list.py +0 -39
  207. google/genai/tests/tokens/__init__.py +0 -16
  208. google/genai/tests/tokens/test_create.py +0 -154
  209. google/genai/tests/transformers/__init__.py +0 -17
  210. google/genai/tests/transformers/test_blobs.py +0 -71
  211. google/genai/tests/transformers/test_bytes.py +0 -15
  212. google/genai/tests/transformers/test_duck_type.py +0 -96
  213. google/genai/tests/transformers/test_function_responses.py +0 -72
  214. google/genai/tests/transformers/test_schema.py +0 -653
  215. google/genai/tests/transformers/test_t_batch.py +0 -286
  216. google/genai/tests/transformers/test_t_content.py +0 -160
  217. google/genai/tests/transformers/test_t_contents.py +0 -398
  218. google/genai/tests/transformers/test_t_part.py +0 -85
  219. google/genai/tests/transformers/test_t_parts.py +0 -87
  220. google/genai/tests/transformers/test_t_tool.py +0 -157
  221. google/genai/tests/transformers/test_t_tools.py +0 -195
  222. google/genai/tests/tunings/__init__.py +0 -16
  223. google/genai/tests/tunings/test_cancel.py +0 -39
  224. google/genai/tests/tunings/test_end_to_end.py +0 -106
  225. google/genai/tests/tunings/test_get.py +0 -67
  226. google/genai/tests/tunings/test_list.py +0 -75
  227. google/genai/tests/tunings/test_tune.py +0 -268
  228. google/genai/tests/types/__init__.py +0 -16
  229. google/genai/tests/types/test_bytes_internal.py +0 -271
  230. google/genai/tests/types/test_bytes_type.py +0 -152
  231. google/genai/tests/types/test_future.py +0 -101
  232. google/genai/tests/types/test_optional_types.py +0 -36
  233. google/genai/tests/types/test_part_type.py +0 -616
  234. google/genai/tests/types/test_schema_from_json_schema.py +0 -417
  235. google/genai/tests/types/test_schema_json_schema.py +0 -468
  236. google/genai/tests/types/test_types.py +0 -2903
  237. google_genai-1.55.0.dist-info/RECORD +0 -345
  238. /google/genai/{tests/interactions/__init__.py → _interactions/py.typed} +0 -0
  239. {google_genai-1.55.0.dist-info → google_genai-1.56.0.dist-info}/WHEEL +0 -0
  240. {google_genai-1.55.0.dist-info → google_genai-1.56.0.dist-info}/licenses/LICENSE +0 -0
  241. {google_genai-1.55.0.dist-info → google_genai-1.56.0.dist-info}/top_level.txt +0 -0
@@ -1,954 +0,0 @@
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