File size: 4,102 Bytes
e48cd48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from __future__ import annotations

import inspect
from typing import Any, Annotated, get_args, get_origin, get_type_hints


def _typename(tp: Any) -> str:
    """Return a readable type name from a type or annotation."""
    try:
        if hasattr(tp, "__name__"):
            return tp.__name__  # e.g. int, str
        if getattr(tp, "__module__", None) and getattr(tp, "__qualname__", None):
            return f"{tp.__module__}.{tp.__qualname__}"
        return str(tp).replace("typing.", "")
    except Exception:
        return str(tp)


def _extract_base_and_meta(annotation: Any) -> tuple[Any, str | None]:
    """Given an annotation, return (base_type, first string metadata) if Annotated, else (annotation, None)."""
    try:
        if get_origin(annotation) is Annotated:
            args = get_args(annotation)
            base = args[0] if args else annotation
            # Grab the first string metadata if present
            for meta in args[1:]:
                if isinstance(meta, str):
                    return base, meta
            return base, None
        return annotation, None
    except Exception:
        return annotation, None


def autodoc(summary: str | None = None, returns: str | None = None, *, force: bool = False):
    """
    Decorator that auto-generates a concise Google-style docstring from a function's
    type hints and Annotated metadata. Useful for Gradio MCP where docstrings are
    used for tool descriptions and parameter docs.

    Args:
        summary: Optional one-line summary for the function. If not provided,
            will generate a simple sentence from the function name.
        returns: Optional return value description. If not provided, only the
            return type will be listed (if available).
        force: When True, overwrite an existing docstring. Default False.

    Returns:
        The original function with its __doc__ populated (unless skipped).
    """

    def decorator(func):
        # Skip if docstring already present and not forcing
        if not force and func.__doc__ and func.__doc__.strip():
            return func

        try:
            # include_extras=True to retain Annotated metadata
            hints = get_type_hints(func, include_extras=True, globalns=getattr(func, "__globals__", None))
        except Exception:
            hints = {}

        sig = inspect.signature(func)

        lines: list[str] = []
        # Summary line
        if summary and summary.strip():
            lines.append(summary.strip())
        else:
            pretty = func.__name__.replace("_", " ").strip().capitalize()
            if not pretty.endswith("."):
                pretty += "."
            lines.append(pretty)

        # Args section
        if sig.parameters:
            lines.append("")
            lines.append("Args:")
            for name, param in sig.parameters.items():
                if name == "self":
                    continue
                annot = hints.get(name, param.annotation)
                base, meta = _extract_base_and_meta(annot)
                tname = _typename(base) if base is not inspect._empty else None
                desc = meta or ""
                if tname and tname != str(inspect._empty):
                    lines.append(f"    {name} ({tname}): {desc}".rstrip())
                else:
                    lines.append(f"    {name}: {desc}".rstrip())

        # Returns section
        ret_hint = hints.get("return", sig.return_annotation)
        if returns or (ret_hint and ret_hint is not inspect.Signature.empty):
            lines.append("")
            lines.append("Returns:")
            if returns:
                lines.append(f"    {returns}")
            else:
                base, meta = _extract_base_and_meta(ret_hint)
                rtype = _typename(base)
                if meta:
                    lines.append(f"    {rtype}: {meta}")
                else:
                    lines.append(f"    {rtype}")

        func.__doc__ = "\n".join(lines).strip() + "\n"
        return func

    return decorator


__all__ = ["autodoc"]