6
6
import textwrap
7
7
import typing
8
8
from 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
11
10
12
11
from sphinx .application import Sphinx
12
+ from sphinx .config import Config
13
13
from sphinx .environment import BuildEnvironment
14
14
from sphinx .ext .autodoc import Options
15
15
from sphinx .util import logging
@@ -90,7 +90,13 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
90
90
return getattr (annotation , "__args__" , ())
91
91
92
92
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
+
94
100
# Special cases
95
101
if annotation is None or annotation is type (None ): # noqa: E721
96
102
return ":py:obj:`None`"
@@ -116,6 +122,7 @@ def format_annotation(annotation: Any, fully_qualified: bool = False, simplify_o
116
122
module = "typing"
117
123
118
124
full_name = f"{ module } .{ class_name } " if module != "builtins" else class_name
125
+ fully_qualified : bool = getattr (config , "fully_qualified" , False )
119
126
prefix = "" if fully_qualified or full_name == class_name else "~"
120
127
role = "data" if class_name in _PYDATA_ANNOTATIONS else "class"
121
128
args_format = "\\ [{}]"
@@ -131,20 +138,21 @@ def format_annotation(annotation: Any, fully_qualified: bool = False, simplify_o
131
138
if len (args ) == 2 :
132
139
full_name = "typing.Optional"
133
140
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
138
147
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 ]} ]"
142
150
elif full_name == "typing.Literal" :
143
151
formatted_args = f"\\ [{ ', ' .join (repr (arg ) for arg in args )} ]"
144
152
145
153
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 ) )
148
156
149
157
return f":py:{ role } :`{ prefix } { full_name } `{ formatted_args } "
150
158
@@ -438,11 +446,6 @@ def process_docstring(
438
446
signature = None
439
447
type_hints = get_all_type_hints (obj , name )
440
448
441
- formatter = partial (
442
- format_annotation ,
443
- fully_qualified = app .config .typehints_fully_qualified ,
444
- simplify_optional_unions = app .config .simplify_optional_unions ,
445
- )
446
449
for arg_name , annotation in type_hints .items ():
447
450
if arg_name == "return" :
448
451
continue # this is handled separately later
@@ -453,7 +456,7 @@ def process_docstring(
453
456
if arg_name .endswith ("_" ):
454
457
arg_name = f"{ arg_name [:- 1 ]} \\ _"
455
458
456
- formatted_annotation = formatter (annotation )
459
+ formatted_annotation = format_annotation (annotation , app . config )
457
460
458
461
search_for = {f":{ field } { arg_name } :" for field in ("param" , "parameter" , "arg" , "argument" )}
459
462
insert_index = None
@@ -480,7 +483,7 @@ def process_docstring(
480
483
if "return" in type_hints and not inspect .isclass (original_obj ):
481
484
if what == "method" and name .endswith (".__init__" ): # avoid adding a return type for data class __init__
482
485
return
483
- formatted_annotation = formatter (type_hints ["return" ])
486
+ formatted_annotation = format_annotation (type_hints ["return" ], app . config )
484
487
insert_index = len (lines )
485
488
for at , line in enumerate (lines ):
486
489
if line .startswith (":rtype:" ):
@@ -506,6 +509,10 @@ def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
506
509
if app .config .typehints_defaults not in valid | {False }:
507
510
raise ValueError (f"typehints_defaults needs to be one of { valid !r} , not { app .config .typehints_defaults !r} " )
508
511
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
+
509
516
510
517
def setup (app : Sphinx ) -> dict [str , bool ]:
511
518
app .add_config_value ("set_type_checking_flag" , False , "html" )
@@ -514,6 +521,7 @@ def setup(app: Sphinx) -> dict[str, bool]:
514
521
app .add_config_value ("typehints_document_rtype" , True , "env" )
515
522
app .add_config_value ("typehints_defaults" , None , "env" )
516
523
app .add_config_value ("simplify_optional_unions" , True , "env" )
524
+ app .add_config_value ("typehints_formatter" , None , "env" )
517
525
app .connect ("builder-inited" , builder_ready )
518
526
app .connect ("env-before-read-docs" , validate_config ) # config may be changed after “config-inited” event
519
527
app .connect ("autodoc-process-signature" , process_signature )
0 commit comments