Skip to content

Commit d3466d7

Browse files
picnixzAA-Turner
authored andcommitted
cleanup
1 parent acc92ff commit d3466d7

File tree

1 file changed

+74
-30
lines changed

1 file changed

+74
-30
lines changed

sphinx/util/typing.py

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,25 @@
88
from collections.abc import Sequence
99
from contextvars import Context, ContextVar, Token
1010
from 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

1322
from docutils import nodes
1423
from docutils.parsers.rst.states import Inliner
1524

1625
if 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

@@ -31,6 +40,9 @@
3140
'smart',
3241
]
3342

43+
class _SpecialFormInterface(Protocol):
44+
_name: str
45+
3446
if sys.version_info >= (3, 10):
3547
from types import UnionType
3648
else:
@@ -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+
167198
def 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

Comments
 (0)