1"""The composite types for Sphinx."""
2
3from __future__ import annotations
4
5import dataclasses
6import sys
7import types
8import typing
9from collections.abc import Sequence
10from contextvars import Context, ContextVar, Token
11from struct import Struct
12from typing import (
13 TYPE_CHECKING,
14 Annotated,
15 Any,
16 Callable,
17 ForwardRef,
18 TypedDict,
19 TypeVar,
20 Union,
21)
22
23from docutils import nodes
24from docutils.parsers.rst.states import Inliner
25
26if TYPE_CHECKING:
27 from collections.abc import Mapping
28 from typing import Final, Literal, Protocol
29
30 from typing_extensions import TypeAlias, TypeIs
31
32 from sphinx.application import Sphinx
33
34 _RestifyMode: TypeAlias = Literal[
35 'fully-qualified-except-typing',
36 'smart',
37 ]
38 _StringifyMode: TypeAlias = Literal[
39 'fully-qualified-except-typing',
40 'fully-qualified',
41 'smart',
42 ]
43
44if sys.version_info >= (3, 10):
45 from types import UnionType
46else:
47 UnionType = None
48
49# classes that have an incorrect .__module__ attribute
50_INVALID_BUILTIN_CLASSES: Final[Mapping[object, str]] = {
51 Context: 'contextvars.Context', # Context.__module__ == '_contextvars'
52 ContextVar: 'contextvars.ContextVar', # ContextVar.__module__ == '_contextvars'
53 Token: 'contextvars.Token', # Token.__module__ == '_contextvars'
54 Struct: 'struct.Struct', # Struct.__module__ == '_struct'
55 # types in 'types' with <type>.__module__ == 'builtins':
56 types.AsyncGeneratorType: 'types.AsyncGeneratorType',
57 types.BuiltinFunctionType: 'types.BuiltinFunctionType',
58 types.BuiltinMethodType: 'types.BuiltinMethodType',
59 types.CellType: 'types.CellType',
60 types.ClassMethodDescriptorType: 'types.ClassMethodDescriptorType',
61 types.CodeType: 'types.CodeType',
62 types.CoroutineType: 'types.CoroutineType',
63 types.FrameType: 'types.FrameType',
64 types.FunctionType: 'types.FunctionType',
65 types.GeneratorType: 'types.GeneratorType',
66 types.GetSetDescriptorType: 'types.GetSetDescriptorType',
67 types.LambdaType: 'types.LambdaType',
68 types.MappingProxyType: 'types.MappingProxyType',
69 types.MemberDescriptorType: 'types.MemberDescriptorType',
70 types.MethodDescriptorType: 'types.MethodDescriptorType',
71 types.MethodType: 'types.MethodType',
72 types.MethodWrapperType: 'types.MethodWrapperType',
73 types.ModuleType: 'types.ModuleType',
74 types.TracebackType: 'types.TracebackType',
75 types.WrapperDescriptorType: 'types.WrapperDescriptorType',
76}
77
78
79def is_invalid_builtin_class(obj: Any) -> bool:
80 """Check *obj* is an invalid built-in class."""
81 try:
82 return obj in _INVALID_BUILTIN_CLASSES
83 except TypeError: # unhashable type
84 return False
85
86
87# Text like nodes which are initialized with text and rawsource
88TextlikeNode = Union[nodes.Text, nodes.TextElement]
89
90# type of None
91NoneType = type(None)
92
93# path matcher
94PathMatcher = Callable[[str], bool]
95
96# common role functions
97if TYPE_CHECKING:
98 class RoleFunction(Protocol):
99 def __call__(
100 self,
101 name: str,
102 rawtext: str,
103 text: str,
104 lineno: int,
105 inliner: Inliner,
106 /,
107 options: dict[str, Any] | None = None,
108 content: Sequence[str] = (),
109 ) -> tuple[list[nodes.Node], list[nodes.system_message]]:
110 ...
111else:
112 RoleFunction = Callable[
113 [str, str, str, int, Inliner, dict[str, Any], Sequence[str]],
114 tuple[list[nodes.Node], list[nodes.system_message]],
115 ]
116
117# A option spec for directive
118OptionSpec = dict[str, Callable[[str], Any]]
119
120# title getter functions for enumerable nodes (see sphinx.domains.std)
121TitleGetter = Callable[[nodes.Node], str]
122
123# inventory data on memory
124InventoryItem = tuple[
125 str, # project name
126 str, # project version
127 str, # URL
128 str, # display name
129]
130Inventory = dict[str, dict[str, InventoryItem]]
131
132
133class ExtensionMetadata(TypedDict, total=False):
134 """The metadata returned by an extension's ``setup()`` function.
135
136 See :ref:`ext-metadata`.
137 """
138
139 version: str
140 """The extension version (default: ``'unknown version'``)."""
141 env_version: int
142 """An integer that identifies the version of env data added by the extension."""
143 parallel_read_safe: bool
144 """Indicate whether parallel reading of source files is supported
145 by the extension.
146 """
147 parallel_write_safe: bool
148 """Indicate whether parallel writing of output files is supported
149 by the extension (default: ``True``).
150 """
151
152
153if TYPE_CHECKING:
154 _ExtensionSetupFunc = Callable[[Sphinx], ExtensionMetadata]
155
156
157def get_type_hints(
158 obj: Any,
159 globalns: dict[str, Any] | None = None,
160 localns: dict[str, Any] | None = None,
161 include_extras: bool = False,
162) -> dict[str, Any]:
163 """Return a dictionary containing type hints for a function, method, module or class
164 object.
165
166 This is a simple wrapper of `typing.get_type_hints()` that does not raise an error on
167 runtime.
168 """
169 from sphinx.util.inspect import safe_getattr # lazy loading
170
171 try:
172 return typing.get_type_hints(obj, globalns, localns, include_extras=include_extras)
173 except NameError:
174 # Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
175 return safe_getattr(obj, '__annotations__', {})
176 except AttributeError:
177 # Failed to evaluate ForwardRef (maybe not runtime checkable)
178 return safe_getattr(obj, '__annotations__', {})
179 except TypeError:
180 # Invalid object is given. But try to get __annotations__ as a fallback.
181 return safe_getattr(obj, '__annotations__', {})
182 except KeyError:
183 # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084)
184 return {}
185
186
187def is_system_TypeVar(typ: Any) -> bool:
188 """Check *typ* is system defined TypeVar."""
189 modname = getattr(typ, '__module__', '')
190 return modname == 'typing' and isinstance(typ, TypeVar)
191
192
193def _is_annotated_form(obj: Any) -> TypeIs[Annotated[Any, ...]]:
194 """Check if *obj* is an annotated type."""
195 return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated')
196
197
198def _is_unpack_form(obj: Any) -> bool:
199 """Check if the object is :class:`typing.Unpack` or equivalent."""
200 if sys.version_info >= (3, 11):
201 from typing import Unpack
202
203 # typing_extensions.Unpack != typing.Unpack for 3.11, but we assume
204 # that typing_extensions.Unpack should not be used in that case
205 return typing.get_origin(obj) is Unpack
206
207 # 3.9 and 3.10 require typing_extensions.Unpack
208 origin = typing.get_origin(obj)
209 return (
210 getattr(origin, '__module__', None) == 'typing_extensions'
211 and _typing_internal_name(origin) == 'Unpack'
212 )
213
214
215def _typing_internal_name(obj: Any) -> str | None:
216 if sys.version_info[:2] >= (3, 10):
217 try:
218 return obj.__name__
219 except AttributeError:
220 # e.g. ParamSpecArgs, ParamSpecKwargs
221 return ''
222 return getattr(obj, '_name', None)
223
224
225def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str:
226 """Convert a type-like object to a reST reference.
227
228 :param mode: Specify a method how annotations will be stringified.
229
230 'fully-qualified-except-typing'
231 Show the module name and qualified name of the annotation except
232 the "typing" module.
233 'smart'
234 Show the name of the annotation.
235 """
236 from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading
237 from sphinx.util import inspect # lazy loading
238
239 valid_modes = {'fully-qualified-except-typing', 'smart'}
240 if mode not in valid_modes:
241 valid = ', '.join(map(repr, sorted(valid_modes)))
242 msg = f'mode must be one of {valid}; got {mode!r}'
243 raise ValueError(msg)
244
245 # things that are not types
246 if cls is None or cls == NoneType:
247 return ':py:obj:`None`'
248 if cls is Ellipsis:
249 return '...'
250 if isinstance(cls, str):
251 return cls
252
253 cls_module_is_typing = getattr(cls, '__module__', '') == 'typing'
254
255 # If the mode is 'smart', we always use '~'.
256 # If the mode is 'fully-qualified-except-typing',
257 # we use '~' only for the objects in the ``typing`` module.
258 module_prefix = '~' if mode == 'smart' or cls_module_is_typing else ''
259
260 try:
261 if ismockmodule(cls):
262 return f':py:class:`{module_prefix}{cls.__name__}`'
263 elif ismock(cls):
264 return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
265 elif is_invalid_builtin_class(cls):
266 # The above predicate never raises TypeError but should not be
267 # evaluated before determining whether *cls* is a mocked object
268 # or not; instead of two try-except blocks, we keep it here.
269 return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`'
270 elif _is_annotated_form(cls):
271 args = restify(cls.__args__[0], mode)
272 meta_args = []
273 for m in cls.__metadata__:
274 if isinstance(m, type):
275 meta_args.append(restify(m, mode))
276 elif dataclasses.is_dataclass(m):
277 # use restify for the repr of field values rather than repr
278 d_fields = ', '.join([
279 fr"{f.name}=\ {restify(getattr(m, f.name), mode)}"
280 for f in dataclasses.fields(m) if f.repr
281 ])
282 meta_args.append(fr'{restify(type(m), mode)}\ ({d_fields})')
283 else:
284 meta_args.append(repr(m))
285 meta = ', '.join(meta_args)
286 if sys.version_info[:2] <= (3, 11):
287 # Hardcoded to fix errors on Python 3.11 and earlier.
288 return fr':py:class:`~typing.Annotated`\ [{args}, {meta}]'
289 return (f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
290 fr'\ [{args}, {meta}]')
291 elif inspect.isNewType(cls):
292 if sys.version_info[:2] >= (3, 10):
293 # newtypes have correct module info since Python 3.10+
294 return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
295 return f':py:class:`{cls.__name__}`'
296 elif UnionType and isinstance(cls, UnionType):
297 # Union types (PEP 585) retain their definition order when they
298 # are printed natively and ``None``-like types are kept as is.
299 return ' | '.join(restify(a, mode) for a in cls.__args__)
300 elif cls.__module__ in ('__builtin__', 'builtins'):
301 if hasattr(cls, '__args__'):
302 if not cls.__args__: # Empty tuple, list, ...
303 return fr':py:class:`{cls.__name__}`\ [{cls.__args__!r}]'
304
305 concatenated_args = ', '.join(restify(arg, mode) for arg in cls.__args__)
306 return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]'
307 return f':py:class:`{cls.__name__}`'
308 elif (inspect.isgenericalias(cls)
309 and cls_module_is_typing
310 and cls.__origin__ is Union):
311 # *cls* is defined in ``typing``, and thus ``__args__`` must exist
312 return ' | '.join(restify(a, mode) for a in cls.__args__)
313 elif inspect.isgenericalias(cls):
314 # A generic alias always has an __origin__, but it is difficult to
315 # use a type guard on inspect.isgenericalias()
316 # (ideally, we would use ``TypeIs`` introduced in Python 3.13).
317 cls_name = _typing_internal_name(cls)
318
319 if isinstance(cls.__origin__, typing._SpecialForm):
320 # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard; TypeIs
321 # Required/NotRequired
322 text = restify(cls.__origin__, mode)
323 elif cls_name:
324 text = f':py:class:`{module_prefix}{cls.__module__}.{cls_name}`'
325 else:
326 text = restify(cls.__origin__, mode)
327
328 __args__ = getattr(cls, '__args__', ())
329 if not __args__:
330 return text
331 if all(map(is_system_TypeVar, __args__)):
332 # Don't print the arguments; they're all system defined type variables.
333 return text
334
335 # Callable has special formatting
336 if (
337 (cls_module_is_typing and _typing_internal_name(cls) == 'Callable')
338 or (cls.__module__ == 'collections.abc' and cls.__name__ == 'Callable')
339 ):
340 args = ', '.join(restify(a, mode) for a in __args__[:-1])
341 returns = restify(__args__[-1], mode)
342 return fr'{text}\ [[{args}], {returns}]'
343
344 if cls_module_is_typing and _typing_internal_name(cls.__origin__) == 'Literal':
345 args = ', '.join(_format_literal_arg_restify(a, mode=mode)
346 for a in cls.__args__)
347 return fr'{text}\ [{args}]'
348
349 # generic representation of the parameters
350 args = ', '.join(restify(a, mode) for a in __args__)
351 return fr'{text}\ [{args}]'
352 elif isinstance(cls, typing._SpecialForm):
353 cls_name = _typing_internal_name(cls)
354 return f':py:obj:`~{cls.__module__}.{cls_name}`'
355 elif sys.version_info[:2] >= (3, 11) and cls is typing.Any:
356 # handle bpo-46998
357 return f':py:obj:`~{cls.__module__}.{cls.__name__}`'
358 elif hasattr(cls, '__qualname__'):
359 return f':py:class:`{module_prefix}{cls.__module__}.{cls.__qualname__}`'
360 elif isinstance(cls, ForwardRef):
361 return f':py:class:`{cls.__forward_arg__}`'
362 else:
363 # not a class (ex. TypeVar) but should have a __name__
364 return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`'
365 except (AttributeError, TypeError):
366 return inspect.object_description(cls)
367
368
369def _format_literal_arg_restify(arg: Any, /, *, mode: str) -> str:
370 from sphinx.util.inspect import isenumattribute # lazy loading
371
372 if isenumattribute(arg):
373 enum_cls = arg.__class__
374 if mode == 'smart' or enum_cls.__module__ == 'typing':
375 # MyEnum.member
376 return f':py:attr:`~{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`'
377 # module.MyEnum.member
378 return f':py:attr:`{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`'
379 return repr(arg)
380
381
382def stringify_annotation(
383 annotation: Any,
384 /,
385 mode: _StringifyMode = 'fully-qualified-except-typing',
386) -> str:
387 """Stringify type annotation object.
388
389 :param annotation: The annotation to stringified.
390 :param mode: Specify a method how annotations will be stringified.
391
392 'fully-qualified-except-typing'
393 Show the module name and qualified name of the annotation except
394 the "typing" module.
395 'smart'
396 Show the name of the annotation.
397 'fully-qualified'
398 Show the module name and qualified name of the annotation.
399 """
400 from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading
401 from sphinx.util.inspect import isNewType # lazy loading
402
403 valid_modes = {'fully-qualified-except-typing', 'fully-qualified', 'smart'}
404 if mode not in valid_modes:
405 valid = ', '.join(map(repr, sorted(valid_modes)))
406 msg = f'mode must be one of {valid}; got {mode!r}'
407 raise ValueError(msg)
408
409 # things that are not types
410 if annotation is None or annotation == NoneType:
411 return 'None'
412 if annotation is Ellipsis:
413 return '...'
414 if isinstance(annotation, str):
415 if annotation.startswith("'") and annotation.endswith("'"):
416 # Might be a double Forward-ref'ed type. Go unquoting.
417 return annotation[1:-1]
418 return annotation
419 if not annotation:
420 return repr(annotation)
421
422 module_prefix = '~' if mode == 'smart' else ''
423
424 # The values below must be strings if the objects are well-formed.
425 annotation_qualname: str = getattr(annotation, '__qualname__', '')
426 annotation_module: str = getattr(annotation, '__module__', '')
427 annotation_name: str = getattr(annotation, '__name__', '')
428 annotation_module_is_typing = annotation_module == 'typing'
429
430 # Extract the annotation's base type by considering formattable cases
431 if isinstance(annotation, TypeVar) and not _is_unpack_form(annotation):
432 # typing_extensions.Unpack is incorrectly determined as a TypeVar
433 if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}:
434 return annotation_name
435 return module_prefix + f'{annotation_module}.{annotation_name}'
436 elif isNewType(annotation):
437 if sys.version_info[:2] >= (3, 10):
438 # newtypes have correct module info since Python 3.10+
439 return module_prefix + f'{annotation_module}.{annotation_name}'
440 return annotation_name
441 elif ismockmodule(annotation):
442 return module_prefix + annotation_name
443 elif ismock(annotation):
444 return module_prefix + f'{annotation_module}.{annotation_name}'
445 elif is_invalid_builtin_class(annotation):
446 return module_prefix + _INVALID_BUILTIN_CLASSES[annotation]
447 elif _is_annotated_form(annotation): # for py39+
448 pass
449 elif annotation_module == 'builtins' and annotation_qualname:
450 args = getattr(annotation, '__args__', None)
451 if args is None:
452 return annotation_qualname
453
454 # PEP 585 generic
455 if not args: # Empty tuple, list, ...
456 return repr(annotation)
457
458 concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args)
459 return f'{annotation_qualname}[{concatenated_args}]'
460 else:
461 # add other special cases that can be directly formatted
462 pass
463
464 module_prefix = f'{annotation_module}.'
465 annotation_forward_arg: str | None = getattr(annotation, '__forward_arg__', None)
466 if annotation_qualname or (annotation_module_is_typing and not annotation_forward_arg):
467 if mode == 'smart':
468 module_prefix = f'~{module_prefix}'
469 if annotation_module_is_typing and mode == 'fully-qualified-except-typing':
470 module_prefix = ''
471 elif _is_unpack_form(annotation) and annotation_module == 'typing_extensions':
472 module_prefix = '~' if mode == 'smart' else ''
473 else:
474 module_prefix = ''
475
476 if annotation_module_is_typing:
477 if annotation_forward_arg:
478 # handle ForwardRefs
479 qualname = annotation_forward_arg
480 else:
481 if internal_name := _typing_internal_name(annotation):
482 qualname = internal_name
483 elif annotation_qualname:
484 qualname = annotation_qualname
485 else:
486 # in this case, we know that the annotation is a member
487 # of ``typing`` and all of them define ``__origin__``
488 qualname = stringify_annotation(
489 annotation.__origin__, 'fully-qualified-except-typing',
490 ).replace('typing.', '') # ex. Union
491 elif annotation_qualname:
492 qualname = annotation_qualname
493 elif hasattr(annotation, '__origin__'):
494 # instantiated generic provided by a user
495 qualname = stringify_annotation(annotation.__origin__, mode)
496 elif UnionType and isinstance(annotation, UnionType): # types.UnionType (for py3.10+)
497 qualname = 'types.UnionType'
498 else:
499 # we weren't able to extract the base type, appending arguments would
500 # only make them appear twice
501 return repr(annotation)
502
503 # Process the generic arguments (if any).
504 # They must be a list or a tuple, otherwise they are considered 'broken'.
505 annotation_args = getattr(annotation, '__args__', ())
506 if annotation_args and isinstance(annotation_args, (list, tuple)):
507 if (
508 qualname in {'Union', 'types.UnionType'}
509 and all(getattr(a, '__origin__', ...) is typing.Literal for a in annotation_args)
510 ):
511 # special case to flatten a Union of Literals into a literal
512 flattened_args = typing.Literal[annotation_args].__args__ # type: ignore[attr-defined]
513 args = ', '.join(_format_literal_arg_stringify(a, mode=mode)
514 for a in flattened_args)
515 return f'{module_prefix}Literal[{args}]'
516 if qualname in {'Optional', 'Union', 'types.UnionType'}:
517 return ' | '.join(stringify_annotation(a, mode) for a in annotation_args)
518 elif qualname == 'Callable':
519 args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1])
520 returns = stringify_annotation(annotation_args[-1], mode)
521 return f'{module_prefix}Callable[[{args}], {returns}]'
522 elif qualname == 'Literal':
523 args = ', '.join(_format_literal_arg_stringify(a, mode=mode)
524 for a in annotation_args)
525 return f'{module_prefix}Literal[{args}]'
526 elif _is_annotated_form(annotation): # for py39+
527 args = stringify_annotation(annotation_args[0], mode)
528 meta_args = []
529 for m in annotation.__metadata__:
530 if isinstance(m, type):
531 meta_args.append(stringify_annotation(m, mode))
532 elif dataclasses.is_dataclass(m):
533 # use stringify_annotation for the repr of field values rather than repr
534 d_fields = ', '.join([
535 f"{f.name}={stringify_annotation(getattr(m, f.name), mode)}"
536 for f in dataclasses.fields(m) if f.repr
537 ])
538 meta_args.append(f'{stringify_annotation(type(m), mode)}({d_fields})')
539 else:
540 meta_args.append(repr(m))
541 meta = ', '.join(meta_args)
542 if sys.version_info[:2] <= (3, 9):
543 if mode == 'smart':
544 return f'~typing.Annotated[{args}, {meta}]'
545 if mode == 'fully-qualified':
546 return f'typing.Annotated[{args}, {meta}]'
547 if sys.version_info[:2] <= (3, 11):
548 if mode == 'fully-qualified-except-typing':
549 return f'Annotated[{args}, {meta}]'
550 module_prefix = module_prefix.replace('builtins', 'typing')
551 return f'{module_prefix}Annotated[{args}, {meta}]'
552 return f'{module_prefix}Annotated[{args}, {meta}]'
553 elif all(is_system_TypeVar(a) for a in annotation_args):
554 # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
555 return module_prefix + qualname
556 else:
557 args = ', '.join(stringify_annotation(a, mode) for a in annotation_args)
558 return f'{module_prefix}{qualname}[{args}]'
559
560 return module_prefix + qualname
561
562
563def _format_literal_arg_stringify(arg: Any, /, *, mode: str) -> str:
564 from sphinx.util.inspect import isenumattribute # lazy loading
565
566 if isenumattribute(arg):
567 enum_cls = arg.__class__
568 if mode == 'smart' or enum_cls.__module__ == 'typing':
569 # MyEnum.member
570 return f'{enum_cls.__qualname__}.{arg.name}'
571 # module.MyEnum.member
572 return f'{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}'
573 return repr(arg)
574
575
576# deprecated name -> (object to return, canonical path or empty string, removal version)
577_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
578 'stringify': (stringify_annotation, 'sphinx.util.typing.stringify_annotation', (8, 0)),
579}
580
581
582def __getattr__(name: str) -> Any:
583 if name not in _DEPRECATED_OBJECTS:
584 msg = f'module {__name__!r} has no attribute {name!r}'
585 raise AttributeError(msg)
586
587 from sphinx.deprecation import _deprecation_warning
588
589 deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name]
590 _deprecation_warning(__name__, name, canonical_name, remove=remove)
591 return deprecated_object