11import inspect
22import json
3+ import logging
4+ import re
35from collections .abc import Awaitable , Callable , Sequence
6+ from contextlib import contextmanager
47from itertools import chain
58from types import GenericAlias
6- from typing import Annotated , Any , cast , get_args , get_origin , get_type_hints
9+ from typing import Annotated , Any , Literal , cast , get_args , get_origin , get_type_hints
710
811import pydantic_core
12+ from griffe import Docstring , DocstringSectionKind , GoogleOptions
913from pydantic import (
1014 BaseModel ,
1115 ConfigDict ,
@@ -225,6 +229,8 @@ def func_metadata(
225229 raise InvalidSignature (f"Unable to evaluate type annotations for callable { func .__name__ !r} " ) from e
226230 params = sig .parameters
227231 dynamic_pydantic_model_params : dict [str , Any ] = {}
232+ param_descriptions = get_param_descriptions (func )
233+
228234 for param in params .values ():
229235 if param .name .startswith ("_" ): # pragma: no cover
230236 raise InvalidSignature (f"Parameter { param .name } of { func .__name__ } cannot start with '_'" )
@@ -246,6 +252,10 @@ def func_metadata(
246252 # Use a prefixed field name
247253 field_name = f"field_{ field_name } "
248254
255+ param_description = param_descriptions .get (param .name )
256+ if not has_description (param .annotation ) and param_description :
257+ field_kwargs ["description" ] = param_description
258+
249259 if param .default is not inspect .Parameter .empty :
250260 dynamic_pydantic_model_params [field_name ] = (
251261 Annotated [(annotation , * field_metadata , Field (** field_kwargs ))],
@@ -327,6 +337,19 @@ def func_metadata(
327337 wrap_output = wrap_output ,
328338 )
329339
340+ def has_description (tp : type ) -> bool :
341+ """
342+ given a type, check if it has already been given a description.
343+ for example like:
344+ var: Annotated[int, Field(description="hey")]
345+ """
346+ if get_origin (tp ) is not Annotated :
347+ return False
348+ for meta in get_args (tp ):
349+ if isinstance (meta , FieldInfo ) and meta .description is not None :
350+ return True
351+ return False
352+
330353
331354def _try_create_model_and_schema (
332355 original_annotation : Any ,
@@ -531,3 +554,137 @@ def _convert_to_content(
531554 result = pydantic_core .to_json (result , fallback = str , indent = 2 ).decode ()
532555
533556 return [TextContent (type = "text" , text = result )]
557+
558+
559+ DocstringStyle = Literal ["google" , "numpy" , "sphinx" ]
560+
561+
562+ @contextmanager
563+ def _disable_griffe_logging ():
564+ """disables griffe logging"""
565+ # Hacky, but suggested here: https://github.com/mkdocstrings/griffe/issues/293#issuecomment-2167668117
566+ old_level = logging .root .getEffectiveLevel ()
567+ logging .root .setLevel (logging .ERROR )
568+ yield
569+ logging .root .setLevel (old_level )
570+
571+
572+ def get_param_descriptions (func : Callable [..., Any ]) -> dict [str , str ]:
573+ """
574+ given a function, return a dictionary of all parameters in the doc string of the function,
575+ and their respective description.
576+ the docstring formats supported are google, sphinx and numpy.
577+ the implementation is taken from pedantic AI.
578+ """
579+ doc = inspect .getdoc (func )
580+ if doc is None :
581+ return {}
582+
583+ docstring_style = _infer_docstring_style (doc )
584+ parser_options = (
585+ GoogleOptions (returns_named_value = False , returns_multiple_items = False ) if docstring_style == "google" else None
586+ )
587+ docstring = Docstring (
588+ doc ,
589+ lineno = 1 ,
590+ parser = docstring_style ,
591+ parser_options = parser_options ,
592+ )
593+
594+ with _disable_griffe_logging ():
595+ sections = docstring .parse ()
596+
597+ params = {}
598+ if parameters := next ((p for p in sections if p .kind == DocstringSectionKind .parameters ), None ):
599+ params = {p .name : p .description for p in parameters .value }
600+
601+ return params
602+
603+
604+ def _infer_docstring_style (doc : str ) -> DocstringStyle :
605+ """Simplistic docstring style inference."""
606+ for pattern , replacements , style in _docstring_style_patterns :
607+ matches = (
608+ re .search (pattern .format (replacement ), doc , re .IGNORECASE | re .MULTILINE ) for replacement in replacements
609+ )
610+ if any (matches ):
611+ return style
612+ # fallback to google style
613+ return "google"
614+
615+
616+ # See https://github.com/mkdocstrings/griffe/issues/329#issuecomment-2425017804
617+ _docstring_style_patterns : list [tuple [str , list [str ], DocstringStyle ]] = [
618+ (
619+ r"\n[ \t]*:{0}([ \t]+\w+)*:([ \t]+.+)?\n" ,
620+ [
621+ "param" ,
622+ "parameter" ,
623+ "arg" ,
624+ "argument" ,
625+ "key" ,
626+ "keyword" ,
627+ "type" ,
628+ "var" ,
629+ "ivar" ,
630+ "cvar" ,
631+ "vartype" ,
632+ "returns" ,
633+ "return" ,
634+ "rtype" ,
635+ "raises" ,
636+ "raise" ,
637+ "except" ,
638+ "exception" ,
639+ ],
640+ "sphinx" ,
641+ ),
642+ (
643+ r"\n[ \t]*{0}:([ \t]+.+)?\n[ \t]+.+" ,
644+ [
645+ "args" ,
646+ "arguments" ,
647+ "params" ,
648+ "parameters" ,
649+ "keyword args" ,
650+ "keyword arguments" ,
651+ "other args" ,
652+ "other arguments" ,
653+ "other params" ,
654+ "other parameters" ,
655+ "raises" ,
656+ "exceptions" ,
657+ "returns" ,
658+ "yields" ,
659+ "receives" ,
660+ "examples" ,
661+ "attributes" ,
662+ "functions" ,
663+ "methods" ,
664+ "classes" ,
665+ "modules" ,
666+ "warns" ,
667+ "warnings" ,
668+ ],
669+ "google" ,
670+ ),
671+ (
672+ r"\n[ \t]*{0}\n[ \t]*---+\n" ,
673+ [
674+ "deprecated" ,
675+ "parameters" ,
676+ "other parameters" ,
677+ "returns" ,
678+ "yields" ,
679+ "receives" ,
680+ "raises" ,
681+ "warns" ,
682+ "attributes" ,
683+ "functions" ,
684+ "methods" ,
685+ "classes" ,
686+ "modules" ,
687+ ],
688+ "numpy" ,
689+ ),
690+ ]
0 commit comments