Skip to content

Commit 6f3cf03

Browse files
committed
docs: add reproduction script for issue #3591
repro_3591.py exercises from_function_with_options() directly on both the old list[str] | None signature and the fixed Optional[List[str]] signature, confirming the parser error and its resolution. Includes Apache 2.0 license header per project convention.
1 parent c332dd5 commit 6f3cf03

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

repro_3591.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2026 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+
"""Reproduction script for issue #3591.
16+
17+
Demonstrates that `list[str] | None` (PEP 604 syntax) triggers a
18+
"Failed to parse the parameter" ValueError in ADK's automatic function
19+
calling parser, while `Optional[List[str]]` (typing module) succeeds.
20+
21+
This exercises `from_function_with_options` directly — the classic ADK
22+
parameter-parsing path that is still the non-experimental default.
23+
24+
Run from the repo root (with ADK installed in dev mode):
25+
python repro_3591.py
26+
"""
27+
28+
from __future__ import annotations
29+
30+
from typing import Any
31+
from typing import List
32+
from typing import Optional
33+
34+
from google.adk.tools.tool_context import ToolContext
35+
36+
37+
# ── BEFORE FIX: PEP 604 union syntax ──────────────────────────────────────────
38+
39+
async def cleanup_unused_files_broken(
40+
used_files: list[str],
41+
tool_context: ToolContext,
42+
file_patterns: list[str] | None = None,
43+
exclude_patterns: list[str] | None = None,
44+
) -> dict[str, Any]:
45+
"""Original signature — triggers parser ValueError."""
46+
...
47+
48+
49+
# ── AFTER FIX: Optional[List[str]] (all sibling tools already use this) ──────
50+
51+
async def cleanup_unused_files_fixed(
52+
used_files: List[str],
53+
tool_context: ToolContext,
54+
file_patterns: Optional[List[str]] = None,
55+
exclude_patterns: Optional[List[str]] = None,
56+
) -> dict[str, Any]:
57+
"""Fixed signature — consistent with write_files, delete_files, read_files."""
58+
...
59+
60+
61+
def main():
62+
from google.adk.tools._automatic_function_calling_util import (
63+
from_function_with_options,
64+
)
65+
from google.adk.utils.variant_utils import GoogleLLMVariant
66+
67+
variant = GoogleLLMVariant.GEMINI_API
68+
ignore = {"tool_context"}
69+
70+
# Helper: strip ToolContext param, which the declaration builder also ignores
71+
import inspect
72+
from types import FunctionType
73+
74+
def strip_tool_context(func):
75+
sig = inspect.signature(func)
76+
new_params = [p for n, p in sig.parameters.items() if n not in ignore]
77+
new_sig = sig.replace(parameters=new_params)
78+
wrapped = FunctionType(
79+
func.__code__, func.__globals__,
80+
func.__name__, func.__defaults__, func.__closure__
81+
)
82+
wrapped.__signature__ = new_sig
83+
wrapped.__doc__ = func.__doc__
84+
wrapped.__annotations__ = {
85+
k: v for k, v in func.__annotations__.items() if k not in ignore
86+
}
87+
return wrapped
88+
89+
# ── BEFORE FIX ────────────────────────────────────────────────────────────
90+
print("=" * 60)
91+
print("[BEFORE FIX] list[str] | None (PEP 604)")
92+
try:
93+
decl = from_function_with_options(
94+
strip_tool_context(cleanup_unused_files_broken), variant
95+
)
96+
print(f" Unexpected success: {decl}")
97+
except ValueError as e:
98+
print(f" ValueError (expected):\n {e}\n")
99+
100+
# ── AFTER FIX ─────────────────────────────────────────────────────────────
101+
print("[AFTER FIX] Optional[List[str]] (typing module)")
102+
try:
103+
decl = from_function_with_options(
104+
strip_tool_context(cleanup_unused_files_fixed), variant
105+
)
106+
props = decl.parameters.properties or {}
107+
fp = props.get("file_patterns")
108+
ep = props.get("exclude_patterns")
109+
print(f" file_patterns -> nullable={fp.nullable}, type={fp.type}")
110+
print(f" exclude_patterns -> nullable={ep.nullable}, type={ep.type}")
111+
print(" OK — parsed without error\n")
112+
except Exception as e:
113+
import traceback
114+
print(f" Unexpected error: {e}")
115+
traceback.print_exc()
116+
117+
print("=" * 60)
118+
print("Conclusion: Replace `list[str] | None` with `Optional[List[str]]`")
119+
print("to fix issue #3591 without waiting for the parser to be updated.")
120+
121+
122+
if __name__ == "__main__":
123+
main()

0 commit comments

Comments
 (0)