Skip to content

Commit 7b98873

Browse files
committed
test: add tests for list[pydantic.BaseModel] support and LiveClient preservation
1 parent 342f545 commit 7b98873

File tree

3 files changed

+506
-0
lines changed

3 files changed

+506
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""Tests to verify both LiveClient classes and list[pydantic.BaseModel] support."""
2+
3+
import inspect
4+
from typing import List, Optional
5+
6+
import pytest
7+
from pydantic import BaseModel, Field
8+
9+
from google import genai
10+
from google.genai import types
11+
12+
13+
@pytest.fixture
14+
def client():
15+
"""Return a client that uses the replay_session."""
16+
client = genai.Client(api_key="test-api-key")
17+
return client
18+
19+
20+
def test_live_client_classes_exist():
21+
"""Verify that LiveClient classes exist and have expected attributes."""
22+
# Check that LiveClientMessage exists
23+
assert hasattr(types, "LiveClientMessage")
24+
assert inspect.isclass(types.LiveClientMessage)
25+
26+
# Check that LiveClientContent exists
27+
assert hasattr(types, "LiveClientContent")
28+
assert inspect.isclass(types.LiveClientContent)
29+
30+
# Check that LiveClientRealtimeInput exists
31+
assert hasattr(types, "LiveClientRealtimeInput")
32+
assert inspect.isclass(types.LiveClientRealtimeInput)
33+
34+
# Check that LiveClientSetup exists
35+
assert hasattr(types, "LiveClientSetup")
36+
assert inspect.isclass(types.LiveClientSetup)
37+
38+
# Check for Dict versions
39+
assert hasattr(types, "LiveClientMessageDict")
40+
assert hasattr(types, "LiveClientContentDict")
41+
assert hasattr(types, "LiveClientRealtimeInputDict")
42+
assert hasattr(types, "LiveClientSetupDict")
43+
44+
45+
def test_live_client_message_fields():
46+
"""Verify that LiveClientMessage has expected fields."""
47+
# Get the field details
48+
fields = types.LiveClientMessage.__fields__
49+
50+
# Check for expected fields
51+
assert "setup" in fields
52+
assert "client_content" in fields
53+
assert "realtime_input" in fields
54+
assert "tool_response" in fields
55+
56+
57+
def test_list_pydantic_in_generate_content_response():
58+
"""Verify that GenerateContentResponse can handle list[pydantic.BaseModel]."""
59+
60+
class Recipe(BaseModel):
61+
recipe_name: str
62+
ingredients: List[str]
63+
64+
# Create a test response
65+
response = types.GenerateContentResponse()
66+
67+
# Assign a list of pydantic models
68+
recipes = [
69+
Recipe(
70+
recipe_name="Chocolate Chip Cookies",
71+
ingredients=["Flour", "Sugar", "Chocolate"],
72+
),
73+
Recipe(
74+
recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"]
75+
),
76+
]
77+
78+
# This assignment would fail with mypy if the type annotation is incorrect
79+
response.parsed = recipes
80+
81+
# Verify assignment worked properly
82+
assert response.parsed is not None
83+
assert isinstance(response.parsed, list)
84+
assert len(response.parsed) == 2
85+
assert all(isinstance(item, Recipe) for item in response.parsed)
86+
87+
88+
def test_combined_functionality(client):
89+
"""Test that combines verification of both LiveClient classes and list[pydantic.BaseModel] support."""
90+
# 1. Verify LiveClient classes exist
91+
assert hasattr(types, "LiveClientMessage")
92+
assert inspect.isclass(types.LiveClientMessage)
93+
94+
# 2. Test list[pydantic.BaseModel] support in generate_content
95+
class Recipe(BaseModel):
96+
recipe_name: str
97+
ingredients: List[str]
98+
instructions: Optional[List[str]] = None
99+
100+
response = client.models.generate_content(
101+
model="gemini-1.5-flash",
102+
contents="List 2 simple cookie recipes.",
103+
config=types.GenerateContentConfig(
104+
response_mime_type="application/json",
105+
response_schema=list[Recipe],
106+
),
107+
)
108+
109+
# Verify the parsed field contains a list of Recipe objects
110+
assert isinstance(response.parsed, list)
111+
assert len(response.parsed) > 0
112+
assert all(isinstance(item, Recipe) for item in response.parsed)
113+
114+
# Access a property to verify the type annotation works correctly
115+
recipe = response.parsed[0]
116+
assert isinstance(recipe.recipe_name, str)
117+
assert isinstance(recipe.ingredients, list)
118+
119+
120+
def test_live_connect_config_exists():
121+
"""Verify that LiveConnectConfig exists and has expected attributes."""
122+
# Check that LiveConnectConfig exists
123+
assert hasattr(types, "LiveConnectConfig")
124+
assert inspect.isclass(types.LiveConnectConfig)
125+
126+
# Check that LiveConnectConfigDict exists
127+
assert hasattr(types, "LiveConnectConfigDict")
128+
129+
# Get the field details if it's a pydantic model
130+
if hasattr(types.LiveConnectConfig, "__fields__"):
131+
fields = types.LiveConnectConfig.__fields__
132+
133+
# Check for expected fields (these might vary based on actual implementation)
134+
assert "model" in fields
135+
136+
137+
def test_live_client_tool_response():
138+
"""Verify that LiveClientToolResponse exists and has expected attributes."""
139+
# Check that LiveClientToolResponse exists
140+
assert hasattr(types, "LiveClientToolResponse")
141+
assert inspect.isclass(types.LiveClientToolResponse)
142+
143+
# Check that LiveClientToolResponseDict exists
144+
assert hasattr(types, "LiveClientToolResponseDict")
145+
146+
# Get the field details
147+
fields = types.LiveClientToolResponse.__fields__
148+
149+
# Check for expected fields (these might vary based on actual implementation)
150+
assert "function_response" in fields or "tool_outputs" in fields
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Tests to verify that mypy correctly handles list[pydantic.BaseModel] in response.parsed."""
2+
3+
from typing import List, cast
4+
import logging
5+
6+
from pydantic import BaseModel
7+
8+
from google.genai import types
9+
10+
# Configure logging
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def test_mypy_with_list_pydantic():
15+
"""
16+
This test doesn't actually run, but it's meant to be analyzed by mypy.
17+
18+
The code patterns here would have caused mypy errors before the fix,
19+
but now should pass type checking with our enhanced types.
20+
"""
21+
22+
# Define a Pydantic model for structured output
23+
class Recipe(BaseModel):
24+
recipe_name: str
25+
ingredients: List[str]
26+
27+
# Create a mock response (simulating what we'd get from the API)
28+
response = types.GenerateContentResponse()
29+
30+
# Before the fix, this next line would cause a mypy error:
31+
# Incompatible types in assignment (expression has type "List[Recipe]",
32+
# variable has type "Optional[Union[BaseModel, Dict[Any, Any], Enum]]")
33+
#
34+
# With our fix adding list[pydantic.BaseModel] to the Union, this is now valid:
35+
response.parsed = [
36+
Recipe(
37+
recipe_name="Chocolate Chip Cookies",
38+
ingredients=["Flour", "Sugar", "Chocolate"],
39+
),
40+
Recipe(
41+
recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"]
42+
),
43+
]
44+
45+
# This pattern would require a type cast before the fix
46+
if response.parsed is not None:
47+
# Before the fix, accessing response.parsed as a list would cause a mypy error
48+
# and require a cast:
49+
# parsed_items = cast(list[Recipe], response.parsed)
50+
51+
# With our fix, we can directly use it as a list without casting:
52+
recipes = response.parsed
53+
54+
# We can iterate over the list without casting
55+
for recipe in recipes:
56+
logger.info(f"Recipe: {recipe.recipe_name}")
57+
for ingredient in recipe.ingredients:
58+
logger.info(f" - {ingredient}")
59+
60+
# We can access elements by index without casting
61+
first_recipe = recipes[0]
62+
logger.info(f"First recipe: {first_recipe.recipe_name}")
63+
64+
65+
def test_with_pydantic_inheritance():
66+
"""Test with inheritance to ensure the type annotation works with subclasses."""
67+
68+
class FoodItem(BaseModel):
69+
name: str
70+
71+
class Recipe(FoodItem):
72+
ingredients: List[str]
73+
74+
response = types.GenerateContentResponse()
75+
76+
# Before the fix, this would require a cast with mypy
77+
# Now it works directly with our enhanced type annotation
78+
response.parsed = [
79+
Recipe(
80+
name="Chocolate Chip Cookies", ingredients=["Flour", "Sugar", "Chocolate"]
81+
),
82+
Recipe(name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"]),
83+
]
84+
85+
if response.parsed is not None:
86+
# Previously would need: cast(list[Recipe], response.parsed)
87+
recipes = response.parsed
88+
89+
# Access fields from parent class
90+
for recipe in recipes:
91+
logger.info(f"Recipe name: {recipe.name}")
92+
93+
94+
def test_with_nested_list_models():
95+
"""Test with nested list models to ensure complex structures work."""
96+
97+
class Ingredient(BaseModel):
98+
name: str
99+
amount: str
100+
101+
class Recipe(BaseModel):
102+
recipe_name: str
103+
ingredients: List[Ingredient]
104+
105+
response = types.GenerateContentResponse()
106+
107+
# With the fix, mypy correctly handles this complex structure
108+
response.parsed = [
109+
Recipe(
110+
recipe_name="Chocolate Chip Cookies",
111+
ingredients=[
112+
Ingredient(name="Flour", amount="2 cups"),
113+
Ingredient(name="Sugar", amount="1 cup"),
114+
],
115+
),
116+
Recipe(
117+
recipe_name="Oatmeal Cookies",
118+
ingredients=[
119+
Ingredient(name="Oats", amount="1 cup"),
120+
Ingredient(name="Flour", amount="1.5 cups"),
121+
],
122+
),
123+
]
124+
125+
if response.parsed is not None:
126+
recipes = response.parsed
127+
128+
# Access nested structures without casting
129+
for recipe in recipes:
130+
logger.info(f"Recipe: {recipe.recipe_name}")
131+
for ingredient in recipe.ingredients:
132+
logger.info(f" - {ingredient.name}: {ingredient.amount}")
133+
134+
135+
# Example of how you would previously need to cast the results
136+
def old_approach_with_cast():
137+
"""
138+
This demonstrates the old approach that required explicit casting,
139+
which was less type-safe and more error-prone.
140+
"""
141+
142+
class Recipe(BaseModel):
143+
recipe_name: str
144+
ingredients: List[str]
145+
146+
response = types.GenerateContentResponse()
147+
148+
# Simulate API response
149+
response.parsed = [
150+
Recipe(
151+
recipe_name="Chocolate Chip Cookies",
152+
ingredients=["Flour", "Sugar", "Chocolate"],
153+
),
154+
Recipe(
155+
recipe_name="Oatmeal Cookies", ingredients=["Oats", "Flour", "Brown Sugar"]
156+
),
157+
]
158+
159+
if response.parsed is not None:
160+
# Before our fix, you'd need this cast for mypy to be happy
161+
recipes = cast(List[Recipe], response.parsed)
162+
163+
# Using the cast list
164+
for recipe in recipes:
165+
logger.info(f"Recipe: {recipe.recipe_name}")

0 commit comments

Comments
 (0)