From 5a57d37d8b01f7fc9da8c5856c708d87c7cc95d6 Mon Sep 17 00:00:00 2001 From: oldrobotdev Date: Fri, 17 Jul 2026 13:38:25 +0200 Subject: [PATCH] fix: parse Google docstrings whose Args: follows a section body --- src/agents/function_schema.py | 20 ++++++++------ tests/test_function_schema.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index 86f791dc43..bcd295f29d 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -159,16 +159,20 @@ def _suppress_griffe_logging(): def _ensure_blank_line_before_google_sections(doc: str) -> str: """Insert a blank line before a Google-style parameter section header (``Args:`` or an - alias) that directly follows non-indented text (for example a summary line). + alias) that directly follows a non-blank line, such as a summary line or the indented body + of a preceding section. griffe's Google parser silently skips a section header when there is no blank line above it and the following line is indented (it logs "Missing blank line above section"). That drops every parameter description and leaks the raw ``Args:`` block into the description. - numpy/sphinx parsing already tolerates the missing blank line, so this normalizes the - Google case to match. Only the parameter section is normalized because - generate_func_documentation only consumes parameter sections (plus the first text block); - other griffe sections are intentionally left alone. The string is returned unchanged when - no insertion is needed, which keeps well-formed docstrings byte-identical. + griffe applies that gate no matter how the line above is indented, so a header that follows + another section's indented body (for example ``Note:`` or ``Example:``) needs the same + normalization as one that follows the summary. numpy/sphinx parsing already tolerates the + missing blank line, so this normalizes the Google case to match. Only the parameter section + is normalized because generate_func_documentation only consumes parameter sections (plus + the first text block); other griffe sections are intentionally left alone. The string is + returned unchanged when no insertion is needed, which keeps well-formed docstrings + byte-identical. """ lines = doc.splitlines() output: list[str] = [] @@ -177,10 +181,10 @@ def _ensure_blank_line_before_google_sections(doc: str) -> str: if ( index > 0 and _GOOGLE_SECTION_HEADER_RE.match(line) - # Preceding line is non-blank top-level text (summary), not an indented section body. + # Preceding line is non-blank, so griffe would skip the header. Its indentation does + # not matter, because the header itself is anchored at column 0 by the regex above. and output and output[-1].strip() - and not output[-1].startswith((" ", "\t")) # Following line is an indented block, matching griffe's "indented line below" gate. and index + 1 < len(lines) and lines[index + 1].startswith((" ", "\t")) diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index 143b6e70f4..674411d1f1 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -958,3 +958,53 @@ def weather(city: str, units: str) -> str: assert properties["city"]["description"] == "The city to get weather for." assert properties["units"]["description"] == "Temperature units to use." assert "Args:" not in (weather.description or "") + + +def section_body_before_args_google_function(city: str, units: str): + """Get the weather for a city. + + Note: + Results are cached. + Args: + city: The city to get weather for. + units: Temperature units to use. + """ + return f"{city} {units}" + + +def section_body_before_args_blank_line_google_function(city: str, units: str): + """Get the weather for a city. + + Note: + Results are cached. + + Args: + city: The city to get weather for. + units: Temperature units to use. + """ + return f"{city} {units}" + + +def test_google_docstring_missing_blank_line_after_section_body(): + """A Google docstring whose ``Args:`` directly follows another section's indented body + (no blank line) should still yield parameter descriptions. griffe skips the header + whenever no blank line sits above it, regardless of how the preceding line is indented.""" + fs = function_schema(section_body_before_args_google_function, strict_json_schema=False) + + properties = fs.params_json_schema.get("properties", {}) + assert properties["city"]["description"] == "The city to get weather for." + assert properties["units"]["description"] == "Temperature units to use." + + assert "Args:" not in (fs.description or "") + + +def test_google_docstring_after_section_body_matches_blank_line_form(): + """The variant missing the blank line after a preceding section's body must produce the + exact same schema as the well-formed variant that includes it.""" + fixed = function_schema(section_body_before_args_google_function, strict_json_schema=False) + control = function_schema( + section_body_before_args_blank_line_google_function, strict_json_schema=False + ) + + assert fixed.description == control.description + assert fixed.params_json_schema["properties"] == control.params_json_schema["properties"]