Skip to content

Commit f0083a2

Browse files
speedstorm1copybara-github
authored andcommitted
feat: add support for Python 3.14.
PiperOrigin-RevId: 819940464
1 parent 57a4765 commit f0083a2

File tree

7 files changed

+150
-28
lines changed

7 files changed

+150
-28
lines changed

.github/workflows/import.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
16+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1717

1818
steps:
1919
- name: Checkout repository

.github/workflows/mypy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
17+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1818

1919
steps:
2020
- name: Checkout code
@@ -29,6 +29,7 @@ jobs:
2929
run: |
3030
python -m pip install --upgrade pip
3131
pip install mypy
32+
sudo apt-get update && sudo apt-get install -y libjpeg-dev zlib1g-dev
3233
pip install -r requirements.txt
3334
3435
- name: Run mypy ${{ matrix.python-version }}

google/genai/_replay_api_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ def _normalize_json_case(obj: Any) -> Any:
5656
return [_normalize_json_case(item) for item in obj]
5757
elif isinstance(obj, enum.Enum):
5858
return obj.value
59-
else:
60-
return obj
59+
elif isinstance(obj, str):
60+
# Python >= 3.14 has a new division by zero error message.
61+
if 'division by zero' in obj:
62+
return obj.replace(
63+
'division by zero', 'integer division or modulo by zero'
64+
)
65+
return obj
6166

6267

6368
def _equals_ignore_key_case(obj1: Any, obj2: Any) -> bool:
@@ -88,7 +93,7 @@ def _equals_ignore_key_case(obj1: Any, obj2: Any) -> bool:
8893

8994
def _redact_version_numbers(version_string: str) -> str:
9095
"""Redacts version numbers in the form x.y.z from a string."""
91-
return re.sub(r'\d+\.\d+\.\d+', '{VERSION_NUMBER}', version_string)
96+
return re.sub(r'\d+\.\d+\.\d+[a-zA-Z0-9]*', '{VERSION_NUMBER}', version_string)
9297

9398

9499
def _redact_language_label(language_label: str) -> str:

google/genai/mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[mypy]
22
exclude = (tests/|_test_api_client\.py)
33
plugins = pydantic.mypy
4-
; we are ignoring 'unused-ignore' because we run mypy on Python 3.9 - 3.13 and
4+
; we are ignoring 'unused-ignore' because we run mypy on Python 3.9 - 3.14 and
55
; some errors in _automatic_function_calling_util.py only apply in 3.10+
66
; 'import-not-found' and 'import-untyped' are environment specific
77
disable_error_code = import-not-found, import-untyped, unused-ignore

google/genai/types.py

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,10 @@ class JSONSchema(_common.BaseModel):
16961696
' matches the instance successfully.'
16971697
),
16981698
)
1699+
additional_properties: Optional[Any] = Field(
1700+
default=None,
1701+
description="""Can either be a boolean or an object; controls the presence of additional properties.""",
1702+
)
16991703
any_of: Optional[list['JSONSchema']] = Field(
17001704
default=None,
17011705
description=(
@@ -1704,6 +1708,20 @@ class JSONSchema(_common.BaseModel):
17041708
' keyword’s value.'
17051709
),
17061710
)
1711+
unique_items: Optional[bool] = Field(
1712+
default=None,
1713+
description="""Boolean value that indicates whether the items in an array are unique.""",
1714+
)
1715+
ref: Optional[str] = Field(
1716+
default=None,
1717+
alias='$ref',
1718+
description="""Allows indirect references between schema nodes.""",
1719+
)
1720+
defs: Optional[dict[str, 'JSONSchema']] = Field(
1721+
default=None,
1722+
alias='$defs',
1723+
description="""Schema definitions to be used with $ref.""",
1724+
)
17071725

17081726

17091727
class Schema(_common.BaseModel):
@@ -1915,7 +1933,7 @@ def from_json_schema(
19151933
list_schema_field_names: tuple[str, ...] = (
19161934
'any_of', # 'one_of', 'all_of', 'not' to come
19171935
)
1918-
dict_schema_field_names: tuple[str, ...] = ('properties',) # 'defs' to come
1936+
dict_schema_field_names: tuple[str, ...] = ('properties',)
19191937

19201938
related_field_names_by_type: dict[str, tuple[str, ...]] = {
19211939
JSONSchemaType.NUMBER.value: (
@@ -1964,6 +1982,23 @@ def from_json_schema(
19641982
# placeholder for potential gemini api unsupported fields
19651983
gemini_api_unsupported_field_names: tuple[str, ...] = ()
19661984

1985+
def _resolve_ref(
1986+
ref_path: str, root_schema_dict: dict[str, Any]
1987+
) -> dict[str, Any]:
1988+
"""Helper to resolve a $ref path."""
1989+
current = root_schema_dict
1990+
for part in ref_path.lstrip('#/').split('/'):
1991+
if part == '$defs':
1992+
part = 'defs'
1993+
current = current[part]
1994+
current.pop('title', None)
1995+
if 'properties' in current and current['properties'] is not None:
1996+
for prop_schema in current['properties'].values():
1997+
if isinstance(prop_schema, dict):
1998+
prop_schema.pop('title', None)
1999+
2000+
return current
2001+
19672002
def normalize_json_schema_type(
19682003
json_schema_type: Optional[
19692004
Union[JSONSchemaType, Sequence[JSONSchemaType], str, Sequence[str]]
@@ -1972,11 +2007,16 @@ def normalize_json_schema_type(
19722007
"""Returns (non_null_types, nullable)"""
19732008
if json_schema_type is None:
19742009
return [], False
1975-
if not isinstance(json_schema_type, Sequence):
1976-
json_schema_type = [json_schema_type]
2010+
type_sequence: Sequence[Union[JSONSchemaType, str]]
2011+
if isinstance(json_schema_type, str) or not isinstance(
2012+
json_schema_type, Sequence
2013+
):
2014+
type_sequence = [json_schema_type]
2015+
else:
2016+
type_sequence = json_schema_type
19772017
non_null_types = []
19782018
nullable = False
1979-
for type_value in json_schema_type:
2019+
for type_value in type_sequence:
19802020
if isinstance(type_value, JSONSchemaType):
19812021
type_value = type_value.value
19822022
if type_value == JSONSchemaType.NULL.value:
@@ -1996,7 +2036,10 @@ def raise_error_if_cannot_convert(
19962036
for field_name, field_value in json_schema_dict.items():
19972037
if field_value is None:
19982038
continue
1999-
if field_name not in google_schema_field_names:
2039+
if field_name not in google_schema_field_names and field_name not in [
2040+
'ref',
2041+
'defs',
2042+
]:
20002043
raise ValueError(
20012044
f'JSONSchema field "{field_name}" is not supported by the '
20022045
'Schema object. And the "raise_error_on_unsupported_field" '
@@ -2026,12 +2069,19 @@ def copy_schema_fields(
20262069
)
20272070

20282071
def convert_json_schema(
2029-
json_schema: JSONSchema,
2072+
current_json_schema: JSONSchema,
2073+
root_json_schema_dict: dict[str, Any],
20302074
api_option: Literal['VERTEX_AI', 'GEMINI_API'],
20312075
raise_error_on_unsupported_field: bool,
20322076
) -> 'Schema':
20332077
schema = Schema()
2034-
json_schema_dict = json_schema.model_dump()
2078+
json_schema_dict = current_json_schema.model_dump()
2079+
2080+
if json_schema_dict.get('ref'):
2081+
json_schema_dict = _resolve_ref(
2082+
json_schema_dict['ref'], root_json_schema_dict
2083+
)
2084+
20352085
raise_error_if_cannot_convert(
20362086
json_schema_dict=json_schema_dict,
20372087
api_option=api_option,
@@ -2057,6 +2107,7 @@ def convert_json_schema(
20572107
non_null_types, nullable = normalize_json_schema_type(
20582108
json_schema_dict.get('type', None)
20592109
)
2110+
is_union_like_type = len(non_null_types) > 1
20602111
if len(non_null_types) > 1:
20612112
logger.warning(
20622113
'JSONSchema type is union-like, e.g. ["null", "string", "array"]. '
@@ -2086,50 +2137,89 @@ def convert_json_schema(
20862137
# Pass 2: the JSONSchema.type is not union-like,
20872138
# e.g. 'string', ['string'], ['null', 'string'].
20882139
for field_name, field_value in json_schema_dict.items():
2089-
if field_value is None:
2140+
if field_value is None or field_name == 'defs':
20902141
continue
20912142
if field_name in schema_field_names:
2143+
if field_name == 'items' and not field_value:
2144+
continue
20922145
schema_field_value: 'Schema' = convert_json_schema(
2093-
json_schema=JSONSchema(**field_value),
2146+
current_json_schema=JSONSchema(**field_value),
2147+
root_json_schema_dict=root_json_schema_dict,
20942148
api_option=api_option,
20952149
raise_error_on_unsupported_field=raise_error_on_unsupported_field,
20962150
)
20972151
setattr(schema, field_name, schema_field_value)
20982152
elif field_name in list_schema_field_names:
20992153
list_schema_field_value: list['Schema'] = [
21002154
convert_json_schema(
2101-
json_schema=JSONSchema(**this_field_value),
2155+
current_json_schema=JSONSchema(**this_field_value),
2156+
root_json_schema_dict=root_json_schema_dict,
21022157
api_option=api_option,
21032158
raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21042159
)
21052160
for this_field_value in field_value
21062161
]
21072162
setattr(schema, field_name, list_schema_field_value)
2163+
if not schema.type and not is_union_like_type:
2164+
schema.type = Type('OBJECT')
21082165
elif field_name in dict_schema_field_names:
21092166
dict_schema_field_value: dict[str, 'Schema'] = {
21102167
key: convert_json_schema(
2111-
json_schema=JSONSchema(**value),
2168+
current_json_schema=JSONSchema(**value),
2169+
root_json_schema_dict=root_json_schema_dict,
21122170
api_option=api_option,
21132171
raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21142172
)
21152173
for key, value in field_value.items()
21162174
}
21172175
setattr(schema, field_name, dict_schema_field_value)
21182176
elif field_name == 'type':
2119-
# non_null_types can only be empty or have one element.
2120-
# because already handled union-like case above.
21212177
non_null_types, nullable = normalize_json_schema_type(field_value)
21222178
if nullable:
21232179
schema.nullable = True
21242180
if non_null_types:
21252181
schema.type = Type(non_null_types[0])
21262182
else:
2127-
setattr(schema, field_name, field_value)
2183+
if (
2184+
hasattr(schema, field_name)
2185+
and field_name != 'additional_properties'
2186+
):
2187+
setattr(schema, field_name, field_value)
2188+
2189+
if (
2190+
schema.type == 'ARRAY'
2191+
and schema.items
2192+
and not schema.items.model_dump(exclude_unset=True)
2193+
):
2194+
schema.items = None
2195+
2196+
if schema.any_of and len(schema.any_of) == 2:
2197+
nullable_part = None
2198+
type_part = None
2199+
for part in schema.any_of:
2200+
# A schema representing `None` will either be of type NULL or just be nullable.
2201+
part_dict = part.model_dump(exclude_unset=True)
2202+
if part_dict == {'nullable': True} or part_dict == {'type': 'NULL'}:
2203+
nullable_part = part
2204+
else:
2205+
type_part = part
2206+
2207+
# If we found both parts, unwrap them into a single schema.
2208+
if nullable_part and type_part:
2209+
default_value = schema.default
2210+
schema = type_part
2211+
schema.nullable = True
2212+
# Carry the default value over to the unwrapped schema
2213+
if default_value is not None:
2214+
schema.default = default_value
21282215

21292216
return schema
21302217

2218+
# This is the initial call to the recursive function.
2219+
root_schema_dict = json_schema.model_dump()
21312220
return convert_json_schema(
2132-
json_schema=json_schema,
2221+
current_json_schema=json_schema,
2222+
root_json_schema_dict=root_schema_dict,
21332223
api_option=api_option,
21342224
raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21352225
)
@@ -2371,7 +2461,32 @@ def from_callable_with_api_option(
23712461
json_schema_dict = _automatic_function_calling_util._add_unevaluated_items_to_fixed_len_tuple_schema(
23722462
json_schema_dict
23732463
)
2374-
parameters_json_schema[name] = json_schema_dict
2464+
if 'prefixItems' in json_schema_dict:
2465+
parameters_json_schema[name] = json_schema_dict
2466+
continue
2467+
2468+
union_args = typing.get_args(param.annotation)
2469+
has_primitive = any(
2470+
_automatic_function_calling_util._is_builtin_primitive_or_compound(
2471+
arg
2472+
)
2473+
for arg in union_args
2474+
)
2475+
if (
2476+
'$ref' in json_schema_dict or '$defs' in json_schema_dict
2477+
) and has_primitive:
2478+
# This is a complex schema with a primitive (e.g., str | MyModel)
2479+
# that is better represented by raw JSON schema.
2480+
parameters_json_schema[name] = json_schema_dict
2481+
continue
2482+
2483+
schema = Schema.from_json_schema(
2484+
json_schema=JSONSchema(**json_schema_dict),
2485+
api_option=api_option,
2486+
)
2487+
if param.default is not inspect.Parameter.empty:
2488+
schema.default = param.default
2489+
parameters_properties[name] = schema
23752490
except Exception as e:
23762491
_automatic_function_calling_util._raise_for_unsupported_param(
23772492
param, callable.__name__, e

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers = [
2222
"Programming Language :: Python :: 3.11",
2323
"Programming Language :: Python :: 3.12",
2424
"Programming Language :: Python :: 3.13",
25+
"Programming Language :: Python :: 3.14",
2526
"Topic :: Internet",
2627
"Topic :: Software Development :: Libraries :: Python Modules",
2728
]
@@ -51,4 +52,4 @@ packages = [
5152
include-package-data = true
5253

5354
[tools.setuptools.package_data]
54-
"google.genai" = ["py.typed"]
55+
"google.genai" = ["py.typed"]

requirements.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ pluggy==1.5.0
1515
py==1.11.0
1616
pyasn1==0.6.1
1717
pyasn1_modules==0.4.1
18-
pydantic==2.11.0
19-
pydantic_core==2.33.0
18+
pydantic==2.12.0
19+
pydantic_core==2.41.1
2020
pytest==8.3.4
2121
pytest-asyncio==0.25.0
2222
pytest-cov==6.0.0
2323
pytest-parallel==0.1.1
2424
requests==2.32.4
2525
rsa==4.9
2626
tenacity==8.2.3
27-
typing_extensions==4.12.2
27+
typing_extensions>=4.14.1
2828
urllib3==2.5.0
2929
websockets==15.0.0
30-
mcp==1.14.0; python_version > '3.9'
31-
sentencepiece >= 0.2.0
30+
mcp>=1.14.0; python_version > '3.9'
31+
sentencepiece>=0.2.0
3232
protobuf

0 commit comments

Comments
 (0)