Add mode parameter to sphinx.util.typing:restify()

To make the typehints in "Bases" field simple, this adds a new parameter
`mode` to sphinx.util.typing:restify() to suppress the leading module
name from typehints in "Bases" field.
This commit is contained in:
Takeshi KOMIYA
2022-01-01 14:24:43 +09:00
parent 8ddf3f09c6
commit 6df45e0ead
2 changed files with 100 additions and 37 deletions

View File

@@ -105,10 +105,24 @@ def is_system_TypeVar(typ: Any) -> bool:
return modname == 'typing' and isinstance(typ, TypeVar)
def restify(cls: Optional[Type]) -> str:
"""Convert python class to a reST reference."""
def restify(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
"""Convert python class to a reST reference.
:param mode: Specify a method how annotations will be stringified.
'fully-qualified-except-typing'
Show the module name and qualified name of the annotation except
the "typing" module.
'smart'
Show the name of the annotation.
"""
from sphinx.util import inspect # lazy loading
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
try:
if cls is None or cls is NoneType:
return ':py:obj:`None`'
@@ -117,63 +131,67 @@ def restify(cls: Optional[Type]) -> str:
elif isinstance(cls, str):
return cls
elif cls in INVALID_BUILTIN_CLASSES:
return ':py:class:`%s`' % INVALID_BUILTIN_CLASSES[cls]
return ':py:class:`%s%s`' % (modprefix, INVALID_BUILTIN_CLASSES[cls])
elif inspect.isNewType(cls):
if sys.version_info > (3, 10):
# newtypes have correct module info since Python 3.10+
print(cls, type(cls), dir(cls))
return ':py:class:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
else:
return ':py:class:`%s`' % cls.__name__
elif UnionType and isinstance(cls, UnionType):
if len(cls.__args__) > 1 and None in cls.__args__:
args = ' | '.join(restify(a) for a in cls.__args__ if a)
args = ' | '.join(restify(a, mode) for a in cls.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(restify(a) for a in cls.__args__)
return ' | '.join(restify(a, mode) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'):
if hasattr(cls, '__args__'):
return ':py:class:`%s`\\ [%s]' % (
cls.__name__,
', '.join(restify(arg) for arg in cls.__args__),
', '.join(restify(arg, mode) for arg in cls.__args__),
)
else:
return ':py:class:`%s`' % cls.__name__
else:
if sys.version_info >= (3, 7): # py37+
return _restify_py37(cls)
return _restify_py37(cls, mode)
else:
return _restify_py36(cls)
return _restify_py36(cls, mode)
except (AttributeError, TypeError):
return inspect.object_description(cls)
def _restify_py37(cls: Optional[Type]) -> str:
def _restify_py37(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
"""Convert python class to a reST reference."""
from sphinx.util import inspect # lazy loading
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
if (inspect.isgenericalias(cls) and
cls.__module__ == 'typing' and cls.__origin__ is Union):
# Union
if len(cls.__args__) > 1 and cls.__args__[-1] is NoneType:
if len(cls.__args__) > 2:
args = ', '.join(restify(a) for a in cls.__args__[:-1])
args = ', '.join(restify(a, mode) for a in cls.__args__[:-1])
return ':py:obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args
else:
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0], mode)
else:
args = ', '.join(restify(a) for a in cls.__args__)
args = ', '.join(restify(a, mode) for a in cls.__args__)
return ':py:obj:`~typing.Union`\\ [%s]' % args
elif inspect.isgenericalias(cls):
if isinstance(cls.__origin__, typing._SpecialForm):
text = restify(cls.__origin__) # type: ignore
text = restify(cls.__origin__, mode) # type: ignore
elif getattr(cls, '_name', None):
if cls.__module__ == 'typing':
text = ':py:class:`~%s.%s`' % (cls.__module__, cls._name)
else:
text = ':py:class:`%s.%s`' % (cls.__module__, cls._name)
text = ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls._name)
else:
text = restify(cls.__origin__)
text = restify(cls.__origin__, mode)
origin = getattr(cls, '__origin__', None)
if not hasattr(cls, '__args__'):
@@ -182,12 +200,12 @@ def _restify_py37(cls: Optional[Type]) -> str:
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
pass
elif cls.__module__ == 'typing' and cls._name == 'Callable':
args = ', '.join(restify(a) for a in cls.__args__[:-1])
text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1]))
args = ', '.join(restify(a, mode) for a in cls.__args__[:-1])
text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1], mode))
elif cls.__module__ == 'typing' and getattr(origin, '_name', None) == 'Literal':
text += r"\ [%s]" % ', '.join(repr(a) for a in cls.__args__)
elif cls.__args__:
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
text += r"\ [%s]" % ", ".join(restify(a, mode) for a in cls.__args__)
return text
elif isinstance(cls, typing._SpecialForm):
@@ -196,7 +214,7 @@ def _restify_py37(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__qualname__)
elif isinstance(cls, ForwardRef):
return ':py:class:`%s`' % cls.__forward_arg__
else:
@@ -204,10 +222,15 @@ def _restify_py37(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
def _restify_py36(cls: Optional[Type]) -> str:
def _restify_py36(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
module = getattr(cls, '__module__', None)
if module == 'typing':
if getattr(cls, '_name', None):
@@ -221,7 +244,7 @@ def _restify_py36(cls: Optional[Type]) -> str:
else:
qualname = repr(cls).replace('typing.', '')
elif hasattr(cls, '__qualname__'):
qualname = '%s.%s' % (module, cls.__qualname__)
qualname = '%s%s.%s' % (modprefix, module, cls.__qualname__)
else:
qualname = repr(cls)
@@ -230,11 +253,11 @@ def _restify_py36(cls: Optional[Type]) -> str:
if module == 'typing':
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':py:class:`%s`' % qualname
reftext = ':py:class:`%s%s`' % (modprefix, qualname)
params = cls.__args__
if params:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return reftext + '\\ [%s]' % param_str
else:
return reftext
@@ -242,19 +265,19 @@ def _restify_py36(cls: Optional[Type]) -> str:
if module == 'typing':
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':py:class:`%s`' % qualname
reftext = ':py:class:`%s%s`' % (modprefix, qualname)
if cls.__args__ is None or len(cls.__args__) <= 2:
params = cls.__args__
elif cls.__origin__ == Generator:
params = cls.__args__
else: # typing.Callable
args = ', '.join(restify(arg) for arg in cls.__args__[:-1])
result = restify(cls.__args__[-1])
args = ', '.join(restify(arg, mode) for arg in cls.__args__[:-1])
result = restify(cls.__args__[-1], mode)
return reftext + '\\ [[%s], %s]' % (args, result)
if params:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return reftext + '\\ [%s]' % (param_str)
else:
return reftext
@@ -264,13 +287,13 @@ def _restify_py36(cls: Optional[Type]) -> str:
if params is not None:
if len(params) > 1 and params[-1] is NoneType:
if len(params) > 2:
param_str = ", ".join(restify(p) for p in params[:-1])
param_str = ", ".join(restify(p, mode) for p in params[:-1])
return (':py:obj:`~typing.Optional`\\ '
'[:py:obj:`~typing.Union`\\ [%s]]' % param_str)
else:
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(params[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(params[0], mode)
else:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return ':py:obj:`~typing.Union`\\ [%s]' % param_str
else:
return ':py:obj:`Union`'
@@ -278,25 +301,25 @@ def _restify_py36(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__qualname__)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls._name)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls._name)
elif hasattr(cls, '__name__'):
# not a class (ex. TypeVar)
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
else:
# others (ex. Any)
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, qualname)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, qualname)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, qualname)
def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> str:

View File

@@ -43,13 +43,28 @@ class BrokenType:
def test_restify():
assert restify(int) == ":py:class:`int`"
assert restify(int, "smart") == ":py:class:`int`"
assert restify(str) == ":py:class:`str`"
assert restify(str, "smart") == ":py:class:`str`"
assert restify(None) == ":py:obj:`None`"
assert restify(None, "smart") == ":py:obj:`None`"
assert restify(Integral) == ":py:class:`numbers.Integral`"
assert restify(Integral, "smart") == ":py:class:`~numbers.Integral`"
assert restify(Struct) == ":py:class:`struct.Struct`"
assert restify(Struct, "smart") == ":py:class:`~struct.Struct`"
assert restify(TracebackType) == ":py:class:`types.TracebackType`"
assert restify(TracebackType, "smart") == ":py:class:`~types.TracebackType`"
assert restify(Any) == ":py:obj:`~typing.Any`"
assert restify(Any, "smart") == ":py:obj:`~typing.Any`"
assert restify('str') == "str"
assert restify('str', "smart") == "str"
def test_restify_type_hints_containers():
@@ -99,13 +114,24 @@ def test_restify_type_hints_Union():
if sys.version_info >= (3, 7):
assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`numbers.Integral`]")
assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`,"
" :py:class:`~numbers.Integral`]")
assert (restify(Union[MyClass1, MyClass2]) ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`tests.test_util_typing.MyClass1`, "
":py:class:`tests.test_util_typing.<MyClass2>`]"))
assert (restify(Union[MyClass1, MyClass2], "smart") ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`~tests.test_util_typing.MyClass1`,"
" :py:class:`~tests.test_util_typing.<MyClass2>`]"))
else:
assert restify(Union[int, Integral]) == ":py:class:`numbers.Integral`"
assert restify(Union[int, Integral], "smart") == ":py:class:`~numbers.Integral`"
assert restify(Union[MyClass1, MyClass2]) == ":py:class:`tests.test_util_typing.MyClass1`"
assert restify(Union[MyClass1, MyClass2], "smart") == ":py:class:`~tests.test_util_typing.MyClass1`"
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@@ -115,19 +141,31 @@ def test_restify_type_hints_typevars():
T_contra = TypeVar('T_contra', contravariant=True)
assert restify(T) == ":py:obj:`tests.test_util_typing.T`"
assert restify(T, "smart") == ":py:obj:`~tests.test_util_typing.T`"
assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`"
assert restify(T_co, "smart") == ":py:obj:`~tests.test_util_typing.T_co`"
assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`"
assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util_typing.T_contra`"
assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]"
if sys.version_info >= (3, 10):
assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`"
else:
assert restify(MyInt) == ":py:class:`MyInt`"
assert restify(MyInt, "smart") == ":py:class:`MyInt`"
def test_restify_type_hints_custom_class():
assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`"
assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util_typing.MyClass1`"
assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`"
assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util_typing.<MyClass2>`"
def test_restify_type_hints_alias():
@@ -169,12 +207,14 @@ def test_restify_type_union_operator():
def test_restify_broken_type_hints():
assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`'
assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util_typing.BrokenType`'
def test_restify_mock():
with mock(['unknown']):
import unknown
assert restify(unknown.secret.Class) == ':py:class:`unknown.secret.Class`'
assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`'
def test_stringify():