66import textwrap
77import typing
88from ast import FunctionDef , Module , stmt
9- from functools import partial
10- from typing import Any , AnyStr , NewType , TypeVar , get_type_hints
9+ from typing import Any , AnyStr , Callable , NewType , TypeVar , get_type_hints
1110
1211from sphinx .application import Sphinx
12+ from sphinx .config import Config
1313from sphinx .environment import BuildEnvironment
1414from sphinx .ext .autodoc import Options
1515from sphinx .util import logging
@@ -90,7 +90,13 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
9090 return getattr (annotation , "__args__" , ())
9191
9292
93- def format_annotation (annotation : Any , fully_qualified : bool = False , simplify_optional_unions : bool = True ) -> str :
93+ def format_annotation (annotation : Any , config : Config ) -> str :
94+ typehints_formatter : Callable [..., str ] | None = getattr (config , "typehints_formatter" , None )
95+ if typehints_formatter is not None :
96+ formatted = typehints_formatter (annotation , config )
97+ if formatted is not None :
98+ return formatted
99+
94100 # Special cases
95101 if annotation is None or annotation is type (None ): # noqa: E721
96102 return ":py:obj:`None`"
@@ -116,6 +122,7 @@ def format_annotation(annotation: Any, fully_qualified: bool = False, simplify_o
116122 module = "typing"
117123
118124 full_name = f"{ module } .{ class_name } " if module != "builtins" else class_name
125+ fully_qualified : bool = getattr (config , "fully_qualified" , False )
119126 prefix = "" if fully_qualified or full_name == class_name else "~"
120127 role = "data" if class_name in _PYDATA_ANNOTATIONS else "class"
121128 args_format = "\\ [{}]"
@@ -131,20 +138,21 @@ def format_annotation(annotation: Any, fully_qualified: bool = False, simplify_o
131138 if len (args ) == 2 :
132139 full_name = "typing.Optional"
133140 args = tuple (x for x in args if x is not type (None )) # noqa: E721
134- elif not simplify_optional_unions :
135- full_name = "typing.Optional"
136- args_format = f"\\ [:py:data:`{ prefix } typing.Union`\\ [{{}}]]"
137- args = tuple (x for x in args if x is not type (None )) # noqa: E721
141+ else :
142+ simplify_optional_unions : bool = getattr (config , "simplify_optional_unions" , True )
143+ if not simplify_optional_unions :
144+ full_name = "typing.Optional"
145+ args_format = f"\\ [:py:data:`{ prefix } typing.Union`\\ [{{}}]]"
146+ args = tuple (x for x in args if x is not type (None )) # noqa: E721
138147 elif full_name == "typing.Callable" and args and args [0 ] is not ...:
139- fmt = ", " .join (format_annotation (arg , simplify_optional_unions = simplify_optional_unions ) for arg in args [:- 1 ])
140- formatted_args = f"\\ [\\ [{ fmt } ]"
141- formatted_args += f", { format_annotation (args [- 1 ], simplify_optional_unions = simplify_optional_unions )} ]"
148+ fmt = [format_annotation (arg , config ) for arg in args ]
149+ formatted_args = f"\\ [\\ [{ ', ' .join (fmt [:- 1 ])} ], { fmt [- 1 ]} ]"
142150 elif full_name == "typing.Literal" :
143151 formatted_args = f"\\ [{ ', ' .join (repr (arg ) for arg in args )} ]"
144152
145153 if args and not formatted_args :
146- fmt = ", " . join ( format_annotation (arg , fully_qualified , simplify_optional_unions ) for arg in args )
147- formatted_args = args_format .format (fmt )
154+ fmt = [ format_annotation (arg , config ) for arg in args ]
155+ formatted_args = args_format .format (", " . join ( fmt ) )
148156
149157 return f":py:{ role } :`{ prefix } { full_name } `{ formatted_args } "
150158
@@ -438,11 +446,6 @@ def process_docstring(
438446 signature = None
439447 type_hints = get_all_type_hints (obj , name )
440448
441- formatter = partial (
442- format_annotation ,
443- fully_qualified = app .config .typehints_fully_qualified ,
444- simplify_optional_unions = app .config .simplify_optional_unions ,
445- )
446449 for arg_name , annotation in type_hints .items ():
447450 if arg_name == "return" :
448451 continue # this is handled separately later
@@ -453,7 +456,7 @@ def process_docstring(
453456 if arg_name .endswith ("_" ):
454457 arg_name = f"{ arg_name [:- 1 ]} \\ _"
455458
456- formatted_annotation = formatter (annotation )
459+ formatted_annotation = format_annotation (annotation , app . config )
457460
458461 search_for = {f":{ field } { arg_name } :" for field in ("param" , "parameter" , "arg" , "argument" )}
459462 insert_index = None
@@ -480,7 +483,7 @@ def process_docstring(
480483 if "return" in type_hints and not inspect .isclass (original_obj ):
481484 if what == "method" and name .endswith (".__init__" ): # avoid adding a return type for data class __init__
482485 return
483- formatted_annotation = formatter (type_hints ["return" ])
486+ formatted_annotation = format_annotation (type_hints ["return" ], app . config )
484487 insert_index = len (lines )
485488 for at , line in enumerate (lines ):
486489 if line .startswith (":rtype:" ):
@@ -506,6 +509,10 @@ def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
506509 if app .config .typehints_defaults not in valid | {False }:
507510 raise ValueError (f"typehints_defaults needs to be one of { valid !r} , not { app .config .typehints_defaults !r} " )
508511
512+ formatter = app .config .typehints_formatter
513+ if formatter is not None and not callable (formatter ):
514+ raise ValueError (f"typehints_formatter needs to be callable or `None`, not { formatter } " )
515+
509516
510517def setup (app : Sphinx ) -> dict [str , bool ]:
511518 app .add_config_value ("set_type_checking_flag" , False , "html" )
@@ -514,6 +521,7 @@ def setup(app: Sphinx) -> dict[str, bool]:
514521 app .add_config_value ("typehints_document_rtype" , True , "env" )
515522 app .add_config_value ("typehints_defaults" , None , "env" )
516523 app .add_config_value ("simplify_optional_unions" , True , "env" )
524+ app .add_config_value ("typehints_formatter" , None , "env" )
517525 app .connect ("builder-inited" , builder_ready )
518526 app .connect ("env-before-read-docs" , validate_config ) # config may be changed after “config-inited” event
519527 app .connect ("autodoc-process-signature" , process_signature )
0 commit comments