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
1726 from collections .abc import Mapping
1827 from typing import Final , Literal
1928
20- from typing_extensions import TypeAlias
29+ from typing_extensions import TypeAlias , TypeGuard
2130
2231 from sphinx .application import Sphinx
2332
@@ -164,6 +173,17 @@ def is_system_TypeVar(typ: Any) -> bool:
164173 return modname == 'typing' and isinstance (typ , TypeVar )
165174
166175
176+ def _is_annotated_form (obj : Any ) -> TypeGuard [Annotated [Any , ...]]:
177+ """Check if *obj* is an annotated type."""
178+ return typing .get_origin (obj ) is Annotated or str (obj ).startswith ('typing.Annotated' )
179+
180+
181+ def _get_typing_internal_name (obj : Any ) -> str | None :
182+ if sys .version_info [:2 ] >= (3 , 10 ):
183+ return obj .__name__
184+ return getattr (obj , '_name' , None )
185+
186+
167187def restify (cls : Any , mode : _RestifyMode = 'fully-qualified-except-typing' ) -> str :
168188 """Convert python class to a reST reference.
169189
@@ -185,35 +205,34 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
185205 raise ValueError (msg )
186206
187207 # things that are not types
188- if cls is None or cls is NoneType :
208+ if cls in { None , NoneType } :
189209 return ':py:obj:`None`'
190210 if cls is Ellipsis :
191211 return '...'
192212 if isinstance (cls , str ):
193213 return cls
194214
215+ cls_module_is_typing = getattr (cls , '__module__' , '' ) == 'typing'
216+
195217 # If the mode is 'smart', we always use '~'.
196218 # If the mode is 'fully-qualified-except-typing',
197219 # we use '~' only for the objects in the ``typing`` module.
198- if mode == 'smart' or getattr (cls , '__module__' , None ) == 'typing' :
199- modprefix = '~'
200- else :
201- modprefix = ''
220+ module_prefix = '~' if mode == 'smart' or cls_module_is_typing else ''
202221
203222 try :
204223 if ismockmodule (cls ):
205- return f':py:class:`{ modprefix } { cls .__name__ } `'
224+ return f':py:class:`{ module_prefix } { cls .__name__ } `'
206225 elif ismock (cls ):
207- return f':py:class:`{ modprefix } { cls .__module__ } .{ cls .__name__ } `'
226+ return f':py:class:`{ module_prefix } { cls .__module__ } .{ cls .__name__ } `'
208227 elif is_invalid_builtin_class (cls ):
209228 # The above predicate never raises TypeError but should not be
210229 # evaluated before determining whether *cls* is a mocked object
211230 # or not; instead of two try-except blocks, we keep it here.
212- return f':py:class:`{ modprefix } { _INVALID_BUILTIN_CLASSES [cls ]} `'
231+ return f':py:class:`{ module_prefix } { _INVALID_BUILTIN_CLASSES [cls ]} `'
213232 elif inspect .isNewType (cls ):
214233 if sys .version_info [:2 ] >= (3 , 10 ):
215234 # newtypes have correct module info since Python 3.10+
216- return f':py:class:`{ modprefix } { cls .__module__ } .{ cls .__name__ } `'
235+ return f':py:class:`{ module_prefix } { cls .__module__ } .{ cls .__name__ } `'
217236 return f':py:class:`{ cls .__name__ } `'
218237 elif UnionType and isinstance (cls , UnionType ):
219238 # Union types (PEP 585) retain their definition order when they
@@ -228,48 +247,56 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
228247 return fr':py:class:`{ cls .__name__ } `\ [{ concatenated_args } ]'
229248 return f':py:class:`{ cls .__name__ } `'
230249 elif (inspect .isgenericalias (cls )
231- and cls . __module__ == 'typing'
250+ and cls_module_is_typing
232251 and cls .__origin__ is Union ):
233252 # *cls* is defined in ``typing``, and thus ``__args__`` must exist
234253 return ' | ' .join (restify (a , mode ) for a in cls .__args__ )
235254 elif inspect .isgenericalias (cls ):
255+ cls_name = _get_typing_internal_name (cls )
256+
236257 if isinstance (cls .__origin__ , typing ._SpecialForm ):
258+ # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard
259+ # Required/NotRequired
237260 text = restify (cls .__origin__ , mode )
238- elif getattr (cls , '_name' , None ):
239- cls_name = cls ._name
240- text = f':py:class:`{ modprefix } { cls .__module__ } .{ cls_name } `'
261+ elif cls_name :
262+ text = f':py:class:`{ module_prefix } { cls .__module__ } .{ cls_name } `'
241263 else :
242264 text = restify (cls .__origin__ , mode )
243265
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' :
266+ __args__ = getattr (cls , '__args__' , ())
267+ if not __args__ :
268+ return text
269+ if all (map (is_system_TypeVar , __args__ )):
270+ # Don't print the arguments; they're all system defined type variables.
271+ return text
272+
273+ # Callable has special formatting
274+ if cls_module_is_typing and _get_typing_internal_name (cls ) == 'Callable' :
275+ args = ', ' .join (restify (a , mode ) for a in __args__ [:- 1 ])
276+ returns = restify (__args__ [- 1 ], mode )
277+ return fr'{ text } \ [[{ args } ], { returns } ]'
278+
279+ if cls_module_is_typing and _get_typing_internal_name (cls .__origin__ ) == 'Literal' :
254280 args = ', ' .join (_format_literal_arg_restify (a , mode = mode )
255281 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__ )} ]"
282+ return fr'{ text } \ [{ args } ]'
259283
260- return text
284+ # generic representation of the parameters
285+ args = ', ' .join (restify (a , mode ) for a in __args__ )
286+ return fr'{ text } \ [{ args } ]'
261287 elif isinstance (cls , typing ._SpecialForm ):
262- return f':py:obj:`~{ cls .__module__ } .{ cls ._name } `' # type: ignore[attr-defined]
288+ cls_name = _get_typing_internal_name (cls )
289+ return f':py:obj:`~{ cls .__module__ } .{ cls_name } `'
263290 elif sys .version_info [:2 ] >= (3 , 11 ) and cls is typing .Any :
264291 # handle bpo-46998
265292 return f':py:obj:`~{ cls .__module__ } .{ cls .__name__ } `'
266293 elif hasattr (cls , '__qualname__' ):
267- return f':py:class:`{ modprefix } { cls .__module__ } .{ cls .__qualname__ } `'
294+ return f':py:class:`{ module_prefix } { cls .__module__ } .{ cls .__qualname__ } `'
268295 elif isinstance (cls , ForwardRef ):
269296 return f':py:class:`{ cls .__forward_arg__ } `'
270297 else :
271298 # not a class (ex. TypeVar)
272- return f':py:obj:`{ modprefix } { cls .__module__ } .{ cls .__name__ } `'
299+ return f':py:obj:`{ module_prefix } { cls .__module__ } .{ cls .__name__ } `'
273300 except (AttributeError , TypeError ):
274301 return inspect .object_description (cls )
275302
@@ -315,7 +342,7 @@ def stringify_annotation(
315342 raise ValueError (msg )
316343
317344 # things that are not types
318- if annotation is None or annotation is NoneType :
345+ if annotation in { None , NoneType } :
319346 return 'None'
320347 if annotation is Ellipsis :
321348 return '...'
@@ -327,17 +354,15 @@ def stringify_annotation(
327354 if not annotation :
328355 return repr (annotation )
329356
330- if mode == 'smart' :
331- module_prefix = '~'
332- else :
333- module_prefix = ''
357+ module_prefix = '~' if mode == 'smart' else ''
334358
335359 # The values below must be strings if the objects are well-formed.
336360 annotation_qualname : str = getattr (annotation , '__qualname__' , '' )
337361 annotation_module : str = getattr (annotation , '__module__' , '' )
338362 annotation_name : str = getattr (annotation , '__name__' , '' )
339363 annotation_module_is_typing = annotation_module == 'typing'
340364
365+ # Extract the annotation's base type by considering formattable cases
341366 if isinstance (annotation , TypeVar ):
342367 if annotation_module_is_typing and mode in {'fully-qualified-except-typing' , 'smart' }:
343368 return annotation_name
@@ -353,7 +378,7 @@ def stringify_annotation(
353378 return module_prefix + f'{ annotation_module } .{ annotation_name } '
354379 elif is_invalid_builtin_class (annotation ):
355380 return module_prefix + _INVALID_BUILTIN_CLASSES [annotation ]
356- elif str (annotation ). startswith ( 'typing.Annotated' ): # for py39+
381+ elif _is_annotated_form (annotation ): # for py39+
357382 pass
358383 elif annotation_module == 'builtins' and annotation_qualname :
359384 args = getattr (annotation , '__args__' , None )
@@ -365,6 +390,9 @@ def stringify_annotation(
365390 return repr (annotation )
366391 concatenated_args = ', ' .join (stringify_annotation (arg , mode ) for arg in args )
367392 return f'{ annotation_qualname } [{ concatenated_args } ]'
393+ else :
394+ # add other special cases that can be directly formatted
395+ pass
368396
369397 module_prefix = f'{ annotation_module } .'
370398 annotation_forward_arg : str | None = getattr (annotation , '__forward_arg__' , None )
@@ -387,6 +415,8 @@ def stringify_annotation(
387415 elif annotation_qualname :
388416 qualname = annotation_qualname
389417 else :
418+ # in this case, we know that the annotation is a member
419+ # of ``typing`` and all of them define ``__origin__``
390420 qualname = stringify_annotation (
391421 annotation .__origin__ , 'fully-qualified-except-typing' ,
392422 ).replace ('typing.' , '' ) # ex. Union
@@ -402,12 +432,11 @@ def stringify_annotation(
402432 # only make them appear twice
403433 return repr (annotation )
404434
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' }:
435+ # Process the generic arguments (if any).
436+ # They must be a list or a tuple, otherwise they are considered 'broken'.
437+ annotation_args = getattr (annotation , '__args__' , ())
438+ if annotation_args and isinstance (annotation_args , (list , tuple )):
439+ if qualname in {'Optional' , 'Union' , 'types.UnionType' }:
411440 return ' | ' .join (stringify_annotation (a , mode ) for a in annotation_args )
412441 elif qualname == 'Callable' :
413442 args = ', ' .join (stringify_annotation (a , mode ) for a in annotation_args [:- 1 ])
@@ -417,7 +446,7 @@ def stringify_annotation(
417446 args = ', ' .join (_format_literal_arg_stringify (a , mode = mode )
418447 for a in annotation_args )
419448 return f'{ module_prefix } Literal[{ args } ]'
420- elif str (annotation ). startswith ( 'typing.Annotated' ): # for py39+
449+ elif _is_annotated_form (annotation ): # for py39+
421450 return stringify_annotation (annotation_args [0 ], mode )
422451 elif all (is_system_TypeVar (a ) for a in annotation_args ):
423452 # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
0 commit comments