Tools / Modules /_docstrings.py
Nymbo's picture
Create _docstrings.py
e48cd48 verified
raw
history blame
4.1 kB
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"]