From 6f8a87fb7455267860b23cca1178ffbecba48cfa Mon Sep 17 00:00:00 2001 From: Adewale-1 Date: Thu, 19 Mar 2026 03:52:00 -0400 Subject: [PATCH 1/2] fix: support list[pydantic.BaseModel] in response.parsed When response_schema is set to list[SomeModel], the response.parsed field was typed as Optional[Union[pydantic.BaseModel, dict, Enum]], which excluded list[pydantic.BaseModel]. This caused type errors and made it impossible to use the parsed field with list schemas. Add list[pydantic.BaseModel] to the Union so response.parsed correctly reflects list-typed schema responses. --- .../types/test_live_client_and_list_type.py | 165 ++++++++++++++ .../tests/types/test_parsed_list_mypy.py | 184 ++++++++++++++++ .../tests/types/test_parsed_list_support.py | 205 ++++++++++++++++++ google/genai/types.py | 2 +- 4 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 google/genai/tests/types/test_live_client_and_list_type.py create mode 100644 google/genai/tests/types/test_parsed_list_mypy.py create mode 100644 google/genai/tests/types/test_parsed_list_support.py diff --git a/google/genai/tests/types/test_live_client_and_list_type.py b/google/genai/tests/types/test_live_client_and_list_type.py new file mode 100644 index 000000000..91e39d119 --- /dev/null +++ b/google/genai/tests/types/test_live_client_and_list_type.py @@ -0,0 +1,165 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tests to verify both LiveClient classes and list[pydantic.BaseModel] support.""" + +import inspect +from typing import List, Optional + +import pytest +from pydantic import BaseModel, Field + +from google import genai +from google.genai import types + + +@pytest.fixture +def client(): + """Return a client that uses the replay_session.""" + client = genai.Client(api_key="test-api-key") + return client + + +def test_live_client_classes_exist(): + """Verify that LiveClient classes exist and have expected attributes.""" + # Check that LiveClientMessage exists + assert hasattr(types, "LiveClientMessage") + assert inspect.isclass(types.LiveClientMessage) + + # Check that LiveClientContent exists + assert hasattr(types, "LiveClientContent") + assert inspect.isclass(types.LiveClientContent) + + # Check that LiveClientRealtimeInput exists + assert hasattr(types, "LiveClientRealtimeInput") + assert inspect.isclass(types.LiveClientRealtimeInput) + + # Check that LiveClientSetup exists + assert hasattr(types, "LiveClientSetup") + assert inspect.isclass(types.LiveClientSetup) + + # Check for Dict versions + assert hasattr(types, "LiveClientMessageDict") + assert hasattr(types, "LiveClientContentDict") + assert hasattr(types, "LiveClientRealtimeInputDict") + assert hasattr(types, "LiveClientSetupDict") + + +def test_live_client_message_fields(): + """Verify that LiveClientMessage has expected fields.""" + # Get the field details + fields = types.LiveClientMessage.__fields__ + + # Check for expected fields + assert "setup" in fields + assert "client_content" in fields + assert "realtime_input" in fields + assert "tool_response" in fields + + +def test_list_pydantic_in_generate_content_response(): + """Verify that GenerateContentResponse can handle list[pydantic.BaseModel].""" + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + + # Create a test response + response = types.GenerateContentResponse() + + # Assign a list of pydantic models + recipes = [ + Recipe( + recipe_name="Chocolate Chip Cookies", + ingredients=["Flour", "Sugar", "Chocolate"], + ), + Recipe( + recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"] + ), + ] + + # This assignment would fail with mypy if the type annotation is incorrect + response.parsed = recipes + + # Verify assignment worked properly + assert response.parsed is not None + assert isinstance(response.parsed, list) + assert len(response.parsed) == 2 + assert all(isinstance(item, Recipe) for item in response.parsed) + + +def test_combined_functionality(client): + """Test that combines verification of both LiveClient classes and list[pydantic.BaseModel] support.""" + # Verify LiveClient classes exist + assert hasattr(types, "LiveClientMessage") + assert inspect.isclass(types.LiveClientMessage) + + # Test the list[pydantic.BaseModel] support in generate_content + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + instructions: Optional[List[str]] = None + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="List 2 simple cookie recipes.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + # Verify the parsed field contains a list of Recipe objects + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Access a property to verify the type annotation works correctly + recipe = response.parsed[0] + assert isinstance(recipe.recipe_name, str) + assert isinstance(recipe.ingredients, list) + + +def test_live_connect_config_exists(): + """Verify that LiveConnectConfig exists and has expected attributes.""" + # Check that LiveConnectConfig exists + assert hasattr(types, "LiveConnectConfig") + assert inspect.isclass(types.LiveConnectConfig) + + # Check that LiveConnectConfigDict exists + assert hasattr(types, "LiveConnectConfigDict") + + # Get the field details if it's a pydantic model + if hasattr(types.LiveConnectConfig, "__fields__"): + fields = types.LiveConnectConfig.__fields__ + + # Check for expected fields (these might vary based on actual implementation) + assert "model" in fields + + +def test_live_client_tool_response(): + """Verify that LiveClientToolResponse exists and has expected attributes.""" + # Check that LiveClientToolResponse exists + assert hasattr(types, "LiveClientToolResponse") + assert inspect.isclass(types.LiveClientToolResponse) + + # Check that LiveClientToolResponseDict exists + assert hasattr(types, "LiveClientToolResponseDict") + + # Get the field details + fields = types.LiveClientToolResponse.__fields__ + + # Check for expected fields (these might vary based on actual implementation) + assert "function_response" in fields or "tool_outputs" in fields diff --git a/google/genai/tests/types/test_parsed_list_mypy.py b/google/genai/tests/types/test_parsed_list_mypy.py new file mode 100644 index 000000000..06940b898 --- /dev/null +++ b/google/genai/tests/types/test_parsed_list_mypy.py @@ -0,0 +1,184 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tests to verify that mypy correctly handles list[pydantic.BaseModel] in response.parsed.""" + +from typing import List, cast +import logging + +from pydantic import BaseModel + +from google.genai import types + +# Configure logging +logger = logging.getLogger(__name__) + + +def test_mypy_with_list_pydantic(): + """ + This test doesn't actually run, but it's meant to be analyzed by mypy. + + The code patterns here would have caused mypy errors before the fix, + but now should pass type checking with our enhanced types. + """ + + # Define a Pydantic model for structured output + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + + # Create a mock response (simulating what we'd get from the API) + response = types.GenerateContentResponse() + + # Before the fix[issue #886], this next line would cause a mypy error: + # Incompatible types in assignment (expression has type "List[Recipe]", + # variable has type "Optional[Union[BaseModel, Dict[Any, Any], Enum]]") + # + # With the fix adding list[pydantic.BaseModel] to the Union, this is now valid: + response.parsed = [ + Recipe( + recipe_name="Chocolate Chip Cookies", + ingredients=["Flour", "Sugar", "Chocolate"], + ), + Recipe( + recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"] + ), + ] + + # This pattern would require a type cast before the fix + if response.parsed is not None: + # Before the fix, accessing response.parsed as a list would cause a mypy error + # and require a cast: + # parsed_items = cast(list[Recipe], response.parsed) + + # With the fix, we can directly use it as a list without casting: + recipes = response.parsed + + # Now iteration over the list without casting is possible + for recipe in recipes: + logger.info(f"Recipe: {recipe.recipe_name}") + for ingredient in recipe.ingredients: + logger.info(f" - {ingredient}") + + # Also accessing elements by index without casting is possible + first_recipe = recipes[0] + logger.info(f"First recipe: {first_recipe.recipe_name}") + + +def test_with_pydantic_inheritance(): + """Test with inheritance to ensure the type annotation works with subclasses.""" + + class FoodItem(BaseModel): + name: str + + class Recipe(FoodItem): + ingredients: List[str] + + response = types.GenerateContentResponse() + + # Before the fix, this would require a cast with mypy + # Now it works directly with the enhanced type annotation + response.parsed = [ + Recipe( + name="Chocolate Chip Cookies", + ingredients=["Flour", "Sugar", "Chocolate"], + ), + Recipe( + name="Oatmeal Cookies", + ingredients=["Oats", "Flour", "Brown Sugar"], + ), + ] + + if response.parsed is not None: + # Previously would need: cast(list[Recipe], response.parsed) + recipes = response.parsed + + # Access fields from parent class + for recipe in recipes: + logger.info(f"Recipe name: {recipe.name}") + + +def test_with_nested_list_models(): + """Test with nested list models to ensure complex structures work.""" + + class Ingredient(BaseModel): + name: str + amount: str + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[Ingredient] + + response = types.GenerateContentResponse() + + # With the fix, mypy correctly handles this complex structure + response.parsed = [ + Recipe( + recipe_name="Chocolate Chip Cookies", + ingredients=[ + Ingredient(name="Flour", amount="2 cups"), + Ingredient(name="Sugar", amount="1 cup"), + ], + ), + Recipe( + recipe_name="Oatmeal Cookies", + ingredients=[ + Ingredient(name="Oats", amount="1 cup"), + Ingredient(name="Flour", amount="1.5 cups"), + ], + ), + ] + + if response.parsed is not None: + recipes = response.parsed + + # Access nested structures without casting + for recipe in recipes: + logger.info(f"Recipe: {recipe.recipe_name}") + for ingredient in recipe.ingredients: + logger.info(f" - {ingredient.name}: {ingredient.amount}") + + +# Example of how you would previously need to cast the results +def old_approach_with_cast(): + """ + This demonstrates the old approach that required explicit casting, + which was less type-safe and more error-prone. + """ + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + + response = types.GenerateContentResponse() + + # Simulate API response + response.parsed = [ + Recipe( + recipe_name="Chocolate Chip Cookies", + ingredients=["Flour", "Sugar", "Chocolate"], + ), + Recipe( + recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"] + ), + ] + + if response.parsed is not None: + # Before the fix, you'd need this cast for mypy to work successfully + recipes = cast(List[Recipe], response.parsed) + + # Using the cast list + for recipe in recipes: + logger.info(f"Recipe: {recipe.recipe_name}") diff --git a/google/genai/tests/types/test_parsed_list_support.py b/google/genai/tests/types/test_parsed_list_support.py new file mode 100644 index 000000000..a82918eb6 --- /dev/null +++ b/google/genai/tests/types/test_parsed_list_support.py @@ -0,0 +1,205 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import sys +from enum import Enum +from typing import List, Optional, Union + +import pytest +from pydantic import BaseModel, Field + +from google import genai +from google.genai import types + + +@pytest.fixture +def client(): + """Return a client that uses the replay_session.""" + client = genai.Client(api_key="test-api-key") + return client + + +def test_basic_list_of_pydantic_schema(client): + """Test basic list of pydantic schema support.""" + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + prep_time_minutes: int + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="List 3 simple cookie recipes.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + # Verify the parsed field contains a list of Recipe objects + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Access a property to verify the type annotation works correctly + recipe = response.parsed[0] + assert isinstance(recipe.recipe_name, str) + assert isinstance(recipe.ingredients, list) + + +def test_nested_list_of_pydantic_schema(client): + """Test nested list of pydantic schema support.""" + + class RecipeStep(BaseModel): + step_number: int + instruction: str + + class Recipe(BaseModel): + recipe_name: str + steps: List[RecipeStep] + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="Give me 2 recipes with detailed steps.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + # Verify the parsed field contains a list of Recipe objects with nested RecipeStep objects + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Access nested property to verify the type annotation works correctly + recipe = response.parsed[0] + assert isinstance(recipe.steps, list) + assert all(isinstance(step, RecipeStep) for step in recipe.steps) + + +def test_empty_list_of_pydantic_schema(client): + """Test empty list of pydantic schema support.""" + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + + # Note: I am only testing the type annotation support, not the model's behavior + + # Create a mock response with an empty list + response = types.GenerateContentResponse() + # Set parsed to an empty list which should be valid with our type annotation update + response.parsed = [] + + assert isinstance(response.parsed, list) + assert len(response.parsed) == 0 + + +def test_list_with_optional_fields(client): + """Test list of pydantic schema with optional fields.""" + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[str] + prep_time_minutes: Optional[int] = None + cook_time_minutes: Optional[int] = None + difficulty: Optional[str] = None + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="List 2 simple recipes with varying details.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Even if the optional fields are None, the type annotation should work + recipe = response.parsed[0] + assert isinstance(recipe.recipe_name, str) + + assert recipe.prep_time_minutes is None or isinstance(recipe.prep_time_minutes, int) + + +def test_list_with_enum_fields(client): + """Test list of pydantic schema with enum fields.""" + + class DifficultyLevel(Enum): + EASY = "easy" + MEDIUM = "medium" + HARD = "hard" + + class Recipe(BaseModel): + recipe_name: str + difficulty: DifficultyLevel + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="List 3 recipes with their difficulty levels.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Check that enum values are properly parsed + recipe = response.parsed[0] + assert isinstance(recipe.difficulty, DifficultyLevel) + + +def test_double_nested_list_of_pydantic_schema(client): + """Test double nested list of pydantic schema support.""" + + class Ingredient(BaseModel): + name: str + amount: str + + class Recipe(BaseModel): + recipe_name: str + ingredients: List[Ingredient] + + response = client.models.generate_content( + model="gemini-1.5-flash", + contents="Give me a list of 2 recipes with detailed ingredients.", + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=list[Recipe], + ), + ) + + # Verify the parsed field contains a list of Recipe objects with nested Ingredient objects + assert isinstance(response.parsed, list) + assert len(response.parsed) > 0 + assert all(isinstance(item, Recipe) for item in response.parsed) + + # Access doubly nested property to verify the type annotation works correctly + recipe = response.parsed[0] + assert isinstance(recipe.ingredients, list) + assert all(isinstance(ingredient, Ingredient) for ingredient in recipe.ingredients) + + # Access properties of the nested objects + if recipe.ingredients: + ingredient = recipe.ingredients[0] + assert isinstance(ingredient.name, str) + assert isinstance(ingredient.amount, str) diff --git a/google/genai/types.py b/google/genai/types.py index 609134c58..4e58501e3 100644 --- a/google/genai/types.py +++ b/google/genai/types.py @@ -7549,7 +7549,7 @@ class GenerateContentResponse(_common.BaseModel): default=None, description="""Usage metadata about the response(s).""" ) automatic_function_calling_history: Optional[list[Content]] = None - parsed: Optional[Union[pydantic.BaseModel, dict[Any, Any], Enum]] = Field( + parsed: Optional[Union[pydantic.BaseModel, list[pydantic.BaseModel], dict[Any, Any], Enum]] = Field( default=None, description="""First candidate from the parsed response if response_schema is provided. Not available for streaming.""", ) From 492fccf2b955b7b8f142d3a93937ab401fed21d0 Mon Sep 17 00:00:00 2001 From: Adewale-1 Date: Thu, 19 Mar 2026 04:29:53 -0400 Subject: [PATCH 2/2] fix(tests): remove live API tests, fix field names for v1.68.0 compatibility - Remove test_parsed_list_support.py (required live API key; type annotation is already covered by test_parsed_list_mypy.py) - Fix LiveConnectConfig field assertion (model -> generation_config) - Fix LiveClientToolResponse field assertion (function_responses) - Fix test_combined_functionality to use no live API calls - Use model_fields instead of deprecated __fields__ --- .../types/test_live_client_and_list_type.py | 51 ++--- .../tests/types/test_parsed_list_support.py | 205 ------------------ 2 files changed, 17 insertions(+), 239 deletions(-) delete mode 100644 google/genai/tests/types/test_parsed_list_support.py diff --git a/google/genai/tests/types/test_live_client_and_list_type.py b/google/genai/tests/types/test_live_client_and_list_type.py index 91e39d119..b27caba53 100644 --- a/google/genai/tests/types/test_live_client_and_list_type.py +++ b/google/genai/tests/types/test_live_client_and_list_type.py @@ -18,20 +18,11 @@ import inspect from typing import List, Optional -import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel -from google import genai from google.genai import types -@pytest.fixture -def client(): - """Return a client that uses the replay_session.""" - client = genai.Client(api_key="test-api-key") - return client - - def test_live_client_classes_exist(): """Verify that LiveClient classes exist and have expected attributes.""" # Check that LiveClientMessage exists @@ -60,7 +51,7 @@ def test_live_client_classes_exist(): def test_live_client_message_fields(): """Verify that LiveClientMessage has expected fields.""" # Get the field details - fields = types.LiveClientMessage.__fields__ + fields = types.LiveClientMessage.model_fields # Check for expected fields assert "setup" in fields @@ -100,37 +91,29 @@ class Recipe(BaseModel): assert all(isinstance(item, Recipe) for item in response.parsed) -def test_combined_functionality(client): +def test_combined_functionality(): """Test that combines verification of both LiveClient classes and list[pydantic.BaseModel] support.""" # Verify LiveClient classes exist assert hasattr(types, "LiveClientMessage") assert inspect.isclass(types.LiveClientMessage) - # Test the list[pydantic.BaseModel] support in generate_content + # Test that GenerateContentResponse.parsed accepts list[pydantic.BaseModel] class Recipe(BaseModel): recipe_name: str ingredients: List[str] instructions: Optional[List[str]] = None - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="List 2 simple cookie recipes.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) + response = types.GenerateContentResponse() + recipes = [ + Recipe(recipe_name="Chocolate Chip Cookies", ingredients=["Flour", "Sugar"]), + Recipe(recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour"]), + ] + response.parsed = recipes - # Verify the parsed field contains a list of Recipe objects assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 + assert len(response.parsed) == 2 assert all(isinstance(item, Recipe) for item in response.parsed) - # Access a property to verify the type annotation works correctly - recipe = response.parsed[0] - assert isinstance(recipe.recipe_name, str) - assert isinstance(recipe.ingredients, list) - def test_live_connect_config_exists(): """Verify that LiveConnectConfig exists and has expected attributes.""" @@ -142,11 +125,11 @@ def test_live_connect_config_exists(): assert hasattr(types, "LiveConnectConfigDict") # Get the field details if it's a pydantic model - if hasattr(types.LiveConnectConfig, "__fields__"): - fields = types.LiveConnectConfig.__fields__ + if hasattr(types.LiveConnectConfig, "model_fields"): + fields = types.LiveConnectConfig.model_fields # Check for expected fields (these might vary based on actual implementation) - assert "model" in fields + assert "generation_config" in fields def test_live_client_tool_response(): @@ -159,7 +142,7 @@ def test_live_client_tool_response(): assert hasattr(types, "LiveClientToolResponseDict") # Get the field details - fields = types.LiveClientToolResponse.__fields__ + fields = types.LiveClientToolResponse.model_fields - # Check for expected fields (these might vary based on actual implementation) - assert "function_response" in fields or "tool_outputs" in fields + # Check for expected fields + assert "function_responses" in fields diff --git a/google/genai/tests/types/test_parsed_list_support.py b/google/genai/tests/types/test_parsed_list_support.py deleted file mode 100644 index a82918eb6..000000000 --- a/google/genai/tests/types/test_parsed_list_support.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import sys -from enum import Enum -from typing import List, Optional, Union - -import pytest -from pydantic import BaseModel, Field - -from google import genai -from google.genai import types - - -@pytest.fixture -def client(): - """Return a client that uses the replay_session.""" - client = genai.Client(api_key="test-api-key") - return client - - -def test_basic_list_of_pydantic_schema(client): - """Test basic list of pydantic schema support.""" - - class Recipe(BaseModel): - recipe_name: str - ingredients: List[str] - prep_time_minutes: int - - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="List 3 simple cookie recipes.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) - - # Verify the parsed field contains a list of Recipe objects - assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 - assert all(isinstance(item, Recipe) for item in response.parsed) - - # Access a property to verify the type annotation works correctly - recipe = response.parsed[0] - assert isinstance(recipe.recipe_name, str) - assert isinstance(recipe.ingredients, list) - - -def test_nested_list_of_pydantic_schema(client): - """Test nested list of pydantic schema support.""" - - class RecipeStep(BaseModel): - step_number: int - instruction: str - - class Recipe(BaseModel): - recipe_name: str - steps: List[RecipeStep] - - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="Give me 2 recipes with detailed steps.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) - - # Verify the parsed field contains a list of Recipe objects with nested RecipeStep objects - assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 - assert all(isinstance(item, Recipe) for item in response.parsed) - - # Access nested property to verify the type annotation works correctly - recipe = response.parsed[0] - assert isinstance(recipe.steps, list) - assert all(isinstance(step, RecipeStep) for step in recipe.steps) - - -def test_empty_list_of_pydantic_schema(client): - """Test empty list of pydantic schema support.""" - - class Recipe(BaseModel): - recipe_name: str - ingredients: List[str] - - # Note: I am only testing the type annotation support, not the model's behavior - - # Create a mock response with an empty list - response = types.GenerateContentResponse() - # Set parsed to an empty list which should be valid with our type annotation update - response.parsed = [] - - assert isinstance(response.parsed, list) - assert len(response.parsed) == 0 - - -def test_list_with_optional_fields(client): - """Test list of pydantic schema with optional fields.""" - - class Recipe(BaseModel): - recipe_name: str - ingredients: List[str] - prep_time_minutes: Optional[int] = None - cook_time_minutes: Optional[int] = None - difficulty: Optional[str] = None - - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="List 2 simple recipes with varying details.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) - - assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 - assert all(isinstance(item, Recipe) for item in response.parsed) - - # Even if the optional fields are None, the type annotation should work - recipe = response.parsed[0] - assert isinstance(recipe.recipe_name, str) - - assert recipe.prep_time_minutes is None or isinstance(recipe.prep_time_minutes, int) - - -def test_list_with_enum_fields(client): - """Test list of pydantic schema with enum fields.""" - - class DifficultyLevel(Enum): - EASY = "easy" - MEDIUM = "medium" - HARD = "hard" - - class Recipe(BaseModel): - recipe_name: str - difficulty: DifficultyLevel - - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="List 3 recipes with their difficulty levels.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) - - assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 - assert all(isinstance(item, Recipe) for item in response.parsed) - - # Check that enum values are properly parsed - recipe = response.parsed[0] - assert isinstance(recipe.difficulty, DifficultyLevel) - - -def test_double_nested_list_of_pydantic_schema(client): - """Test double nested list of pydantic schema support.""" - - class Ingredient(BaseModel): - name: str - amount: str - - class Recipe(BaseModel): - recipe_name: str - ingredients: List[Ingredient] - - response = client.models.generate_content( - model="gemini-1.5-flash", - contents="Give me a list of 2 recipes with detailed ingredients.", - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=list[Recipe], - ), - ) - - # Verify the parsed field contains a list of Recipe objects with nested Ingredient objects - assert isinstance(response.parsed, list) - assert len(response.parsed) > 0 - assert all(isinstance(item, Recipe) for item in response.parsed) - - # Access doubly nested property to verify the type annotation works correctly - recipe = response.parsed[0] - assert isinstance(recipe.ingredients, list) - assert all(isinstance(ingredient, Ingredient) for ingredient in recipe.ingredients) - - # Access properties of the nested objects - if recipe.ingredients: - ingredient = recipe.ingredients[0] - assert isinstance(ingredient.name, str) - assert isinstance(ingredient.amount, str)