2424
2525if TYPE_CHECKING :
2626 from collections .abc import Mapping
27- from typing import Final , Protocol
27+ from collections .abc import Set as AbstractSet
28+ from typing import Final , Literal , Protocol
2829
2930 from typing_extensions import TypeGuard
3031
3334 class _SpecialFormInterface (Protocol ):
3435 _name : str
3536
37+ _RestifyMode = Literal ['fully-qualified-except-typing' , 'smart' ]
38+ _StringifyMode = Literal ['fully-qualified-except-typing' , 'fully-qualified' , 'smart' ]
39+ _MT_co = TypeVar ('_MT_co' , bound = Any , covariant = True )
3640
3741if sys .version_info >= (3 , 10 ):
3842 from types import UnionType
@@ -186,7 +190,7 @@ def _get_typing_internal_name(obj: Any) -> str | None:
186190 return getattr (obj , '_name' , None )
187191
188192
189- def restify (cls : Any , mode : str = 'fully-qualified-except-typing' ) -> str :
193+ def restify (cls : Any , mode : object = 'fully-qualified-except-typing' ) -> str :
190194 """Convert a python type-like object to a reST reference.
191195
192196 :param mode: Specify a method how annotations will be stringified.
@@ -200,10 +204,10 @@ def restify(cls: Any, mode: str = 'fully-qualified-except-typing') -> str:
200204 from sphinx .ext .autodoc .mock import ismock , ismockmodule # lazy loading
201205 from sphinx .util import inspect # lazy loading
202206
203- if mode == 'smart' :
204- modprefix = '~ '
205- else :
206- modprefix = ''
207+ mode = _validate_restify_mode ( mode )
208+ # With an if-else block, mypy infers 'mode' to be a 'str '
209+ # instead of a literal string (and we don't want to cast).
210+ module_prefix = '~' if mode == 'smart' else ''
207211
208212 try :
209213 if cls is None or cls is NoneType :
@@ -213,15 +217,18 @@ def restify(cls: Any, mode: str = 'fully-qualified-except-typing') -> str:
213217 elif isinstance (cls , str ):
214218 return cls
215219 elif ismockmodule (cls ):
216- return f':py:class:`{ modprefix } { cls .__name__ } `'
220+ return f':py:class:`{ module_prefix } { cls .__name__ } `'
217221 elif ismock (cls ):
218- return f':py:class:`{ modprefix } { cls .__module__ } .{ cls .__name__ } `'
219- elif is_invalid_builtin_class (cls ): # this never raises TypeError
220- return f':py:class:`{ modprefix } { _INVALID_BUILTIN_CLASSES [cls ]} `'
222+ return f':py:class:`{ module_prefix } { cls .__module__ } .{ cls .__name__ } `'
223+ elif is_invalid_builtin_class (cls ):
224+ # The above predicate never raises TypeError but should not be
225+ # evaluated before determining whether *cls* is a mocked object
226+ # or not; instead of two try-except blocks, we keep it here.
227+ return f':py:class:`{ module_prefix } { _INVALID_BUILTIN_CLASSES [cls ]} `'
221228 elif inspect .isNewType (cls ):
222229 if sys .version_info [:2 ] >= (3 , 10 ):
223230 # newtypes have correct module info since Python 3.10+
224- return f':py:class:`{ modprefix } { cls .__module__ } .{ cls .__name__ } `'
231+ return f':py:class:`{ module_prefix } { cls .__module__ } .{ cls .__name__ } `'
225232 return f':py:class:`{ cls .__name__ } `'
226233 elif UnionType and isinstance (cls , UnionType ):
227234 # Union types (PEP 585) retain their definition order when they
@@ -264,7 +271,7 @@ def restify(cls: Any, mode: str = 'fully-qualified-except-typing') -> str:
264271 if _is_annotated_form (__origin__ ):
265272 text = restify (__origin__ , mode )
266273 elif internal_class_name := _get_typing_internal_name (cls ):
267- prefix = '~' if cls .__module__ == 'typing' else modprefix
274+ prefix = '~' if cls .__module__ == 'typing' else module_prefix
268275 text = f':py:class:`{ prefix } { cls .__module__ } .{ internal_class_name } `'
269276 else :
270277 text = restify (__origin__ , mode )
@@ -290,13 +297,13 @@ def restify(cls: Any, mode: str = 'fully-qualified-except-typing') -> str:
290297 # handle bpo-46998
291298 return f':py:obj:`~{ cls .__module__ } .{ cls .__name__ } `'
292299 elif hasattr (cls , '__qualname__' ):
293- prefix = '~' if cls .__module__ == 'typing' else modprefix
300+ prefix = '~' if cls .__module__ == 'typing' else module_prefix
294301 return f':py:class:`{ prefix } { cls .__module__ } .{ cls .__qualname__ } `'
295302 elif isinstance (cls , ForwardRef ):
296303 return f':py:class:`{ cls .__forward_arg__ } `'
297304 else :
298305 # not a class (ex. TypeVar)
299- prefix = '~' if cls .__module__ == 'typing' else modprefix
306+ prefix = '~' if cls .__module__ == 'typing' else module_prefix
300307 return f':py:obj:`{ prefix } { cls .__module__ } .{ cls .__name__ } `'
301308 except (AttributeError , TypeError ):
302309 return inspect .object_description (cls )
@@ -323,16 +330,10 @@ def stringify_annotation(
323330 from sphinx .ext .autodoc .mock import ismock , ismockmodule # lazy loading
324331 from sphinx .util .inspect import isNewType # lazy loading
325332
326- allowed_modes = {'fully-qualified-except-typing' , 'fully-qualified' , 'smart' }
327- if mode not in allowed_modes :
328- allowed = ', ' .join (map (repr , sorted (allowed_modes )))
329- msg = f'%r must be one of %s; got { mode !r} ' % ('mode' , allowed )
330- raise ValueError (msg )
331-
332- if mode == 'smart' :
333- module_prefix = '~'
334- else :
335- module_prefix = ''
333+ mode = _validate_stringify_mode (mode )
334+ # With an if-else block, mypy infers 'mode' to be a 'str'
335+ # instead of a literal string (and we don't want to cast).
336+ module_prefix = '~' if mode == 'smart' else ''
336337
337338 # The values below must be strings if the objects are well-formed.
338339 annotation_qualname : str = getattr (annotation , '__qualname__' , '' )
@@ -440,29 +441,57 @@ def stringify_annotation(
440441 return f'{ module_prefix } { qualname } '
441442
442443
443- def _stringify_literal_arg (arg : Any , mode : str ) -> str :
444+ def _restify_literal_arg (arg : Any , mode : _RestifyMode ) -> str :
444445 from sphinx .util .inspect import isenumattribute # lazy loading
445446
446447 if isenumattribute (arg ):
447- enumcls = arg .__class__
448- if mode == 'smart' :
449- # MyEnum.member
450- return f'{ enumcls .__qualname__ } .{ arg .name } '
451- # module.MyEnum.member
452- return f'{ enumcls .__module__ } .{ enumcls .__qualname__ } .{ arg .name } '
448+ enum_cls = arg .__class__
449+ prefix = '~' if mode == 'smart' or enum_cls .__module__ == 'typing' else ''
450+ return f':py:attr:`{ prefix } { enum_cls .__module__ } .{ enum_cls .__qualname__ } .{ arg .name } `'
453451 return repr (arg )
454452
455453
456- def _restify_literal_arg (arg : Any , mode : str ) -> str :
454+ def _stringify_literal_arg (arg : Any , mode : _StringifyMode ) -> str :
457455 from sphinx .util .inspect import isenumattribute # lazy loading
458456
459457 if isenumattribute (arg ):
460458 enum_cls = arg .__class__
461- prefix = '~' if mode == 'smart' or enum_cls .__module__ == 'typing' else ''
462- return f':py:attr:`{ prefix } { enum_cls .__module__ } .{ enum_cls .__qualname__ } .{ arg .name } `'
459+ # 'MyEnum.member' in 'smart' mode, otherwise 'module.MyEnum.member'
460+ prefix = '' if mode == 'smart' else enum_cls .__module__
461+ return f'{ prefix } .{ enum_cls .__qualname__ } .{ arg .name } '
463462 return repr (arg )
464463
465464
465+ # validation functions (keep them as separate functions to handle enumerations
466+ # values instead of plain strings in the future and to be able to add better
467+ # type annotations using literal values)
468+ def _validate_mode (mode : Any , allowed_modes : AbstractSet [_MT_co ]) -> _MT_co :
469+ if mode not in allowed_modes :
470+ allowed = ', ' .join (map (repr , sorted (allowed_modes )))
471+ msg = f'%r must be one of %s; got { mode !r} ' % ('mode' , allowed )
472+ raise ValueError (msg )
473+ return mode
474+
475+
476+ def _validate_restify_mode (mode : Any ) -> _RestifyMode :
477+ # add more allowed modes here (e.g., enumeration values)
478+ allowed_modes : AbstractSet [_RestifyMode ] = {
479+ 'fully-qualified-except-typing' ,
480+ 'smart' ,
481+ }
482+ return _validate_mode (mode , allowed_modes )
483+
484+
485+ def _validate_stringify_mode (mode : Any ) -> _StringifyMode :
486+ # add more allowed modes here (e.g., enumeration values)
487+ allowed_modes : AbstractSet [_StringifyMode ] = {
488+ 'fully-qualified-except-typing' ,
489+ 'fully-qualified' ,
490+ 'smart' ,
491+ }
492+ return _validate_mode (mode , allowed_modes )
493+
494+
466495# deprecated name -> (object to return, canonical path or empty string, removal version)
467496_DEPRECATED_OBJECTS : dict [str , tuple [Any , str , tuple [int , int ]]] = {
468497 'stringify' : (stringify_annotation , 'sphinx.util.typing.stringify_annotation' , (8 , 0 )),
0 commit comments