88from collections .abc import Sequence
99from contextvars import Context , ContextVar , Token
1010from struct import Struct
11- from typing import TYPE_CHECKING , Any , Callable , ForwardRef , TypedDict , TypeVar , Union
11+ from typing import (
12+ TYPE_CHECKING ,
13+ Annotated ,
14+ Any ,
15+ Callable ,
16+ ForwardRef ,
17+ TypedDict ,
18+ TypeVar ,
19+ Union ,
20+ )
1221
1322from docutils import nodes
1423from docutils .parsers .rst .states import Inliner
1524
1625if TYPE_CHECKING :
1726 from collections .abc import Mapping
18- from typing import Final , Literal
27+ from typing import Final , Literal , Protocol
1928
20- from typing_extensions import TypeAlias
29+ from typing_extensions import TypeAlias , TypeGuard
2130
2231 from sphinx .application import Sphinx
2332
3140 'smart' ,
3241 ]
3342
43+ class _SpecialFormInterface (Protocol ):
44+ _name : str
45+
3446if sys .version_info >= (3 , 10 ):
3547 from types import UnionType
3648else :
@@ -164,6 +176,25 @@ def is_system_TypeVar(typ: Any) -> bool:
164176 return modname == 'typing' and isinstance (typ , TypeVar )
165177
166178
179+ def _is_special_form (obj : Any ) -> TypeGuard [_SpecialFormInterface ]:
180+ """Check if *obj* is a typing special form.
181+
182+ The guarded type is a protocol with the members that Sphinx needs in
183+ this module and not the native ``typing._SpecialForm`` from typeshed,
184+ but the runtime type of *obj* must be a true special form instance.
185+ """
186+ return isinstance (obj , typing ._SpecialForm )
187+
188+
189+ def _is_annotated_form (obj : Any ) -> TypeGuard [Annotated [Any , ...]]:
190+ """Check if *obj* is an annotated type."""
191+ return typing .get_origin (obj ) is Annotated or str (obj ).startswith ('typing.Annotated' )
192+
193+
194+ def _get_typing_internal_name (obj : Any ) -> str | None :
195+ return getattr (obj , '_name' , None )
196+
197+
167198def restify (cls : Any , mode : _RestifyMode = 'fully-qualified-except-typing' ) -> str :
168199 """Convert python class to a reST reference.
169200
@@ -185,7 +216,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
185216 raise ValueError (msg )
186217
187218 # things that are not types
188- if cls is None or cls is NoneType :
219+ if cls in { None , NoneType } :
189220 return ':py:obj:`None`'
190221 if cls is Ellipsis :
191222 return '...'
@@ -241,25 +272,32 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
241272 else :
242273 text = restify (cls .__origin__ , mode )
243274
244- origin = getattr (cls , '__origin__' , None )
245- if not hasattr (cls , '__args__' ): # NoQA: SIM114
246- pass
247- elif all (is_system_TypeVar (a ) for a in cls .__args__ ):
248- # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
249- pass
250- elif cls .__module__ == 'typing' and cls ._name == 'Callable' :
251- args = ', ' .join (restify (a , mode ) for a in cls .__args__ [:- 1 ])
252- text += fr'\ [[{ args } ], { restify (cls .__args__ [- 1 ], mode )} ]'
253- elif cls .__module__ == 'typing' and getattr (origin , '_name' , None ) == 'Literal' :
254- args = ', ' .join (_format_literal_arg_restify (a , mode = mode )
255- for a in cls .__args__ )
256- text += fr"\ [{ args } ]"
257- elif cls .__args__ :
258- text += fr"\ [{ ', ' .join (restify (a , mode ) for a in cls .__args__ )} ]"
259-
260- return text
261- elif isinstance (cls , typing ._SpecialForm ):
262- return f':py:obj:`~{ cls .__module__ } .{ cls ._name } `' # type: ignore[attr-defined]
275+ __args__ = getattr (cls , '__args__' , ())
276+ if not __args__ :
277+ return text
278+
279+ if all (map (is_system_TypeVar , __args__ )):
280+ # do not print the arguments they are all type variables
281+ return text
282+
283+ params : str | None = None
284+
285+ if cls .__module__ == 'typing' :
286+ if _get_typing_internal_name (cls ) == 'Callable' :
287+ vargs = ', ' .join (restify (a , mode ) for a in __args__ [:- 1 ])
288+ rtype = restify (__args__ [- 1 ], mode )
289+ params = f'[{ vargs } ], { rtype } '
290+ elif _get_typing_internal_name (cls .__origin__ ) == 'Literal' :
291+ params = ', ' .join (_format_literal_arg_restify (a , mode )
292+ for a in cls .__args__ )
293+
294+ if params is None :
295+ # generic representation of the parameters
296+ params = ', ' .join (restify (a , mode ) for a in __args__ )
297+
298+ return rf'{ text } \ [{ params } ]'
299+ elif _is_special_form (cls ):
300+ return f':py:obj:`~{ cls .__module__ } .{ cls ._name } `'
263301 elif sys .version_info [:2 ] >= (3 , 11 ) and cls is typing .Any :
264302 # handle bpo-46998
265303 return f':py:obj:`~{ cls .__module__ } .{ cls .__name__ } `'
@@ -315,7 +353,7 @@ def stringify_annotation(
315353 raise ValueError (msg )
316354
317355 # things that are not types
318- if annotation is None or annotation is NoneType :
356+ if annotation in { None , NoneType } :
319357 return 'None'
320358 if annotation is Ellipsis :
321359 return '...'
@@ -338,6 +376,7 @@ def stringify_annotation(
338376 annotation_name : str = getattr (annotation , '__name__' , '' )
339377 annotation_module_is_typing = annotation_module == 'typing'
340378
379+ # extract the annotation's base type by considering formattable cases
341380 if isinstance (annotation , TypeVar ):
342381 if annotation_module_is_typing and mode in {'fully-qualified-except-typing' , 'smart' }:
343382 return annotation_name
@@ -365,6 +404,9 @@ def stringify_annotation(
365404 return repr (annotation )
366405 concatenated_args = ', ' .join (stringify_annotation (arg , mode ) for arg in args )
367406 return f'{ annotation_qualname } [{ concatenated_args } ]'
407+ else :
408+ # add other special cases that can be directly formatted
409+ pass
368410
369411 module_prefix = f'{ annotation_module } .'
370412 annotation_forward_arg : str | None = getattr (annotation , '__forward_arg__' , None )
@@ -387,6 +429,8 @@ def stringify_annotation(
387429 elif annotation_qualname :
388430 qualname = annotation_qualname
389431 else :
432+ # in this case, we know that the annotation is a member
433+ # of :mod:`typing` and all of them define ``__origin__``
390434 qualname = stringify_annotation (
391435 annotation .__origin__ , 'fully-qualified-except-typing' ,
392436 ).replace ('typing.' , '' ) # ex. Union
@@ -402,12 +446,12 @@ def stringify_annotation(
402446 # only make them appear twice
403447 return repr (annotation )
404448
405- annotation_args = getattr ( annotation , '__args__' , None )
406- if annotation_args :
407- if not isinstance ( annotation_args , ( list , tuple )):
408- # broken __args__ found
409- pass
410- elif qualname in {'Optional' , 'Union' , 'types.UnionType' }:
449+ # process the generic arguments (if any); they must be a list or a tuple
450+ # otherwise they are considered as 'broken'
451+
452+ annotation_args = getattr ( annotation , ' __args__' , ())
453+ if annotation_args and isinstance ( annotation_args , ( list , tuple )):
454+ if qualname in {'Optional' , 'Union' , 'types.UnionType' }:
411455 return ' | ' .join (stringify_annotation (a , mode ) for a in annotation_args )
412456 elif qualname == 'Callable' :
413457 args = ', ' .join (stringify_annotation (a , mode ) for a in annotation_args [:- 1 ])
0 commit comments