This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new d26160dbf39 Parse HookToolset :param descriptions by indentation 
(#76701)
d26160dbf39 is described below

commit d26160dbf39474687378a7906586a78253fdc5ba
Author: Aarmen Sidhu <[email protected]>
AuthorDate: Mon Nov 9 12:42:19 2026 -0700

    Parse HookToolset :param descriptions by indentation (#76701)
    
    HookToolset matched each :param description with a regex that ran until the
    next line starting with ":" or the end of the docstring. So the last
    parameter absorbed everything after it (601 characters, including a code
    block, for apply_wildcard in S3Hook.list_keys, the docs' own example), a
    continuation line starting with a role such as :class: cut a description
    short, and a parameter with no description took the next field as its own.
    
    Follow reStructuredText field lists instead: a description continues while
    lines are indented deeper than its :param line.
    
    Generated-by: Claude Code following the guidelines
    
    * Keep Google-style Args when a :param has no description
    
    An empty :param field still left an entry in params, so the Google-style
    fallback was skipped and then the empty entry was filtered out, returning
    nothing. Drop empty descriptions before deciding whether to fall back.
    
    Reported by @himanshuchawla0807-gif.
    
    Generated-by: Claude Code following the guidelines
    
    * Fix erf_uri param docs and test a description continuing after a blank 
line
    
    GoogleDisplayVideo360Hook.erf_uri's entity_type continuation line sat at
    the same indent as its :param line, so the field ended after the first
    line, and partner_id was missing its colon. A scan of every provider hook
    found no other docstring with either problem.
    
    The new parser case covers a description whose second paragraph follows a
    blank line, shaped like S3Hook.copy_object's source_bucket_key.
    
    Generated-by: Claude Code following the guidelines
    
    ---------
    
    Co-authored-by: aarmens702-hub 
<[email protected]>
---
 .../airflow/providers/common/ai/toolsets/hook.py   | 22 +++++--
 .../ai/tests/unit/common/ai/toolsets/test_hook.py  | 69 ++++++++++++++++++++++
 .../marketing_platform/hooks/display_video.py      |  4 +-
 3 files changed, 87 insertions(+), 8 deletions(-)

diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/toolsets/hook.py 
b/providers/common/ai/src/airflow/providers/common/ai/toolsets/hook.py
index aa35dcc80b6..7c7f57c6c4b 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/toolsets/hook.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/toolsets/hook.py
@@ -502,7 +502,7 @@ def _extract_description(method: Callable[..., Any]) -> str:
 
 
 # Matches Sphinx-style `:param name:` and Google-style `name:` under an 
``Args:`` block.
-_SPHINX_PARAM_RE = re.compile(r":param\s+(\w+):\s*(.+?)(?=\n\s*:|$)", 
re.DOTALL)
+_SPHINX_PARAM_RE = re.compile(r"^(\s*):param\s+(\w+):(.*)$")
 _GOOGLE_ARGS_RE = re.compile(r"^\s{2,}(\w+)\s*(?:\(.+?\))?:\s*(.+)", 
re.MULTILINE)
 
 
@@ -510,12 +510,22 @@ def _parse_param_docs(docstring: str) -> dict[str, str]:
     """Parse parameter descriptions from Sphinx or Google-style docstrings."""
     params: dict[str, str] = {}
 
-    # Try Sphinx style first.
-    for match in _SPHINX_PARAM_RE.finditer(docstring):
-        name = match.group(1)
-        desc = " ".join(match.group(2).split())
-        params[name] = desc
+    # Try Sphinx style first. As in a reStructuredText field list, a 
description runs on
+    # through the lines indented deeper than its ``:param`` line, so neither a 
continuation
+    # line starting with a role such as ``:class:`` nor an example block after 
the last
+    # parameter changes where it ends.
+    name: str | None = None
+    indent = 0
+    for line in docstring.splitlines():
+        if match := _SPHINX_PARAM_RE.match(line):
+            name, indent = match.group(2), len(match.group(1))
+            params[name] = match.group(3)
+        elif name and (not line.strip() or len(line) - len(line.lstrip()) > 
indent):
+            params[name] += " " + line
+        else:
+            name = None
 
+    params = {param: desc for param, text in params.items() if (desc := " 
".join(text.split()))}
     if params:
         return params
 
diff --git a/providers/common/ai/tests/unit/common/ai/toolsets/test_hook.py 
b/providers/common/ai/tests/unit/common/ai/toolsets/test_hook.py
index 9722a8a5167..2a781988c91 100644
--- a/providers/common/ai/tests/unit/common/ai/toolsets/test_hook.py
+++ b/providers/common/ai/tests/unit/common/ai/toolsets/test_hook.py
@@ -782,6 +782,75 @@ class TestParseParamDocs:
         assert result["name"] == "The name of the thing."
         assert result["count"] == "How many items."
 
+    def test_sphinx_last_param_stops_at_dedented_text(self):
+        # Shaped like S3Hook.list_keys, whose example block followed 
apply_wildcard into its description.
+        docstring = """List keys.
+
+        :param prefix: a key prefix
+        :param apply_wildcard: whether to treat '*' as a wildcard.
+
+        **Example**: Returns the matching keys:
+
+        .. code-block:: python
+
+            hook.list_keys(prefix="data/*", apply_wildcard=True)
+
+        :return: a list of matched keys
+        """
+        result = _parse_param_docs(docstring)
+        assert result == {"prefix": "a key prefix", "apply_wildcard": "whether 
to treat '*' as a wildcard."}
+
+    def test_sphinx_continuation_line_starting_with_a_role(self):
+        # Shaped like BigQueryHook.create_routine, whose description was cut 
off at "Either a".
+        docstring = """Create a routine.
+
+        :param routine: The routine to create. Either a
+            :class:`~google.cloud.bigquery.routine.Routine` instance or a dict.
+        :param routine_id: The routine identifier.
+        """
+        result = _parse_param_docs(docstring)
+        assert result["routine"] == (
+            "The routine to create. Either a 
:class:`~google.cloud.bigquery.routine.Routine` instance or a dict."
+        )
+        assert result["routine_id"] == "The routine identifier."
+
+    def test_sphinx_description_continues_after_a_blank_line(self):
+        # Shaped like S3Hook.copy_object, whose source_bucket_key has an 
indented second paragraph.
+        docstring = """Copy an object.
+
+        :param source_bucket_key: The key of the source object.
+
+            It can be either full s3:// style url or relative path from root 
level.
+        :param dest_bucket_key: The key of the object to copy to.
+        """
+        assert _parse_param_docs(docstring) == {
+            "source_bucket_key": (
+                "The key of the source object. "
+                "It can be either full s3:// style url or relative path from 
root level."
+            ),
+            "dest_bucket_key": "The key of the object to copy to.",
+        }
+
+    def test_sphinx_param_without_description_is_left_out(self):
+        # Shaped like GKEHook._append_label, whose val was described as 
":return: The cluster proto ...".
+        docstring = """Append a label.
+
+        :param key: The key label
+        :param val:
+        :return: The cluster proto updated with new label
+        """
+        assert _parse_param_docs(docstring) == {"key": "The key label"}
+
+    def test_empty_sphinx_param_does_not_hide_google_args(self):
+        docstring = """Read records.
+
+        Args:
+            path: Path to read.
+
+        :param undocumented:
+        """
+        assert _parse_param_docs(docstring) == {"path": "Path to read."}
+
     def test_google_style(self):
         docstring = """Do something.
 
diff --git 
a/providers/google/src/airflow/providers/google/marketing_platform/hooks/display_video.py
 
b/providers/google/src/airflow/providers/google/marketing_platform/hooks/display_video.py
index 6ae63441342..bb9d33428e6 100644
--- 
a/providers/google/src/airflow/providers/google/marketing_platform/hooks/display_video.py
+++ 
b/providers/google/src/airflow/providers/google/marketing_platform/hooks/display_video.py
@@ -72,9 +72,9 @@ class GoogleDisplayVideo360Hook(GoogleBaseHook):
         More information:
         https://developers.google.com/bid-manager/guides/entity-read/overview
 
-        :param partner_id The numeric ID of your Partner.
+        :param partner_id: The numeric ID of your Partner.
         :param entity_type: The type of file Partner, Advertiser, 
InsertionOrder,
-        LineItem, Creative, Pixel, InventorySource, UserList, 
UniversalChannel, and summary.
+            LineItem, Creative, Pixel, InventorySource, UserList, 
UniversalChannel, and summary.
         """
         return [f"gdbm-{partner_id}/entity/{{{{ ds_nodash 
}}}}.*.{entity_type}.json"]
 

Reply via email to