Fix #9630: autodoc: Failed to build xrefs if primary_domain is not 'py'

Autodoc generates reST code that uses raw `:obj:` and `:class:` xrefs to
refer the classes and types.  But they're fragile because they assume
the primary_domain=='py'.

This adds `:py:` prefix to these xrefs to make them robust.
This commit is contained in:
Takeshi KOMIYA 2021-09-14 23:39:47 +09:00
parent ba2439a105
commit ed227d7d3c
7 changed files with 126 additions and 117 deletions

View File

@ -16,6 +16,9 @@ Features added
Bugs fixed
----------
* #9630: autodoc: Failed to build cross references if :confval:`primary_domain`
is not 'py'
Testing
--------

View File

@ -110,18 +110,18 @@ def restify(cls: Optional[Type]) -> str:
try:
if cls is None or cls is NoneType:
return ':obj:`None`'
return ':py:obj:`None`'
elif cls is Ellipsis:
return '...'
elif cls in INVALID_BUILTIN_CLASSES:
return ':class:`%s`' % INVALID_BUILTIN_CLASSES[cls]
return ':py:class:`%s`' % 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 ':class:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:class:`%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':class:`%s`' % cls.__name__
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)
@ -130,12 +130,12 @@ def restify(cls: Optional[Type]) -> str:
return ' | '.join(restify(a) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'):
if hasattr(cls, '__args__'):
return ':class:`%s`\\ [%s]' % (
return ':py:class:`%s`\\ [%s]' % (
cls.__name__,
', '.join(restify(arg) for arg in cls.__args__),
)
else:
return ':class:`%s`' % cls.__name__
return ':py:class:`%s`' % cls.__name__
else:
if sys.version_info >= (3, 7): # py37+
return _restify_py37(cls)
@ -155,20 +155,20 @@ def _restify_py37(cls: Optional[Type]) -> str:
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])
return ':obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args
return ':py:obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args
else:
return ':obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0])
else:
args = ', '.join(restify(a) for a in cls.__args__)
return ':obj:`~typing.Union`\\ [%s]' % args
return ':py:obj:`~typing.Union`\\ [%s]' % args
elif inspect.isgenericalias(cls):
if isinstance(cls.__origin__, typing._SpecialForm):
text = restify(cls.__origin__) # type: ignore
elif getattr(cls, '_name', None):
if cls.__module__ == 'typing':
text = ':class:`~%s.%s`' % (cls.__module__, cls._name)
text = ':py:class:`~%s.%s`' % (cls.__module__, cls._name)
else:
text = ':class:`%s.%s`' % (cls.__module__, cls._name)
text = ':py:class:`%s.%s`' % (cls.__module__, cls._name)
else:
text = restify(cls.__origin__)
@ -188,20 +188,20 @@ def _restify_py37(cls: Optional[Type]) -> str:
return text
elif isinstance(cls, typing._SpecialForm):
return ':obj:`~%s.%s`' % (cls.__module__, cls._name)
return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name)
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
elif isinstance(cls, ForwardRef):
return ':class:`%s`' % cls.__forward_arg__
return ':py:class:`%s`' % cls.__forward_arg__
else:
# not a class (ex. TypeVar)
if cls.__module__ == 'typing':
return ':obj:`~%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
def _restify_py36(cls: Optional[Type]) -> str:
@ -225,9 +225,9 @@ def _restify_py36(cls: Optional[Type]) -> str:
if (isinstance(cls, typing.TupleMeta) and # type: ignore
not hasattr(cls, '__tuple_params__')):
if module == 'typing':
reftext = ':class:`~typing.%s`' % qualname
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':class:`%s`' % qualname
reftext = ':py:class:`%s`' % qualname
params = cls.__args__
if params:
@ -237,9 +237,9 @@ def _restify_py36(cls: Optional[Type]) -> str:
return reftext
elif isinstance(cls, typing.GenericMeta):
if module == 'typing':
reftext = ':class:`~typing.%s`' % qualname
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':class:`%s`' % qualname
reftext = ':py:class:`%s`' % qualname
if cls.__args__ is None or len(cls.__args__) <= 2:
params = cls.__args__
@ -262,38 +262,38 @@ def _restify_py36(cls: Optional[Type]) -> str:
if len(params) > 1 and params[-1] is NoneType:
if len(params) > 2:
param_str = ", ".join(restify(p) for p in params[:-1])
return (':obj:`~typing.Optional`\\ '
'[:obj:`~typing.Union`\\ [%s]]' % param_str)
return (':py:obj:`~typing.Optional`\\ '
'[:py:obj:`~typing.Union`\\ [%s]]' % param_str)
else:
return ':obj:`~typing.Optional`\\ [%s]' % restify(params[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(params[0])
else:
param_str = ', '.join(restify(p) for p in params)
return ':obj:`~typing.Union`\\ [%s]' % param_str
return ':py:obj:`~typing.Union`\\ [%s]' % param_str
else:
return ':obj:`Union`'
return ':py:obj:`Union`'
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
return ':obj:`~%s.%s`' % (cls.__module__, cls._name)
return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name)
else:
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
return ':py:obj:`%s.%s`' % (cls.__module__, cls._name)
elif hasattr(cls, '__name__'):
# not a class (ex. TypeVar)
if cls.__module__ == 'typing':
return ':obj:`~%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
else:
# others (ex. Any)
if cls.__module__ == 'typing':
return ':obj:`~%s.%s`' % (cls.__module__, qualname)
return ':py:obj:`~%s.%s`' % (cls.__module__, qualname)
else:
return ':obj:`%s.%s`' % (cls.__module__, qualname)
return ':py:obj:`%s.%s`' % (cls.__module__, qualname)
def stringify(annotation: Any) -> str:

View File

@ -984,7 +984,7 @@ def test_autodoc_inner_class(app):
' .. py:attribute:: Outer.factory',
' :module: target',
'',
' alias of :class:`dict`'
' alias of :py:class:`dict`'
]
actual = do_autodoc(app, 'class', 'target.Outer.Inner', options)
@ -1009,7 +1009,7 @@ def test_autodoc_inner_class(app):
'',
'.. py:class:: InnerChild()',
' :module: target', '',
' Bases: :class:`target.Outer.Inner`',
' Bases: :py:class:`target.Outer.Inner`',
'',
' InnerChild docstring',
'',
@ -1750,7 +1750,7 @@ def test_autodoc_typed_instance_variables(app):
'.. py:attribute:: Alias',
' :module: target.typed_vars',
'',
' alias of :class:`target.typed_vars.Derived`',
' alias of :py:class:`target.typed_vars.Derived`',
'',
'.. py:class:: Class()',
' :module: target.typed_vars',
@ -1915,12 +1915,12 @@ def test_autodoc_GenericAlias(app):
' .. py:attribute:: Class.T',
' :module: target.genericalias',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
'.. py:attribute:: T',
' :module: target.genericalias',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
]
else:
assert list(actual) == [
@ -1937,7 +1937,7 @@ def test_autodoc_GenericAlias(app):
'',
' A list of int',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
'',
'.. py:data:: T',
@ -1945,7 +1945,7 @@ def test_autodoc_GenericAlias(app):
'',
' A list of int',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
]
@ -1977,7 +1977,7 @@ def test_autodoc_TypeVar(app):
'',
' T6',
'',
' alias of :class:`int`',
' alias of :py:class:`int`',
'',
'',
'.. py:data:: T1',
@ -2017,7 +2017,7 @@ def test_autodoc_TypeVar(app):
'',
' T6',
'',
' alias of :class:`int`',
' alias of :py:class:`int`',
'',
'',
'.. py:data:: T7',
@ -2025,7 +2025,7 @@ def test_autodoc_TypeVar(app):
'',
' T7',
'',
" alias of TypeVar('T7', bound=\\ :class:`int`)",
" alias of TypeVar('T7', bound=\\ :py:class:`int`)",
'',
]

View File

@ -167,7 +167,7 @@ def test_autoattribute_GenericAlias(app):
'',
' A list of int',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
]
@ -182,7 +182,7 @@ def test_autoattribute_NewType(app):
'',
' T6',
'',
' alias of :class:`int`',
' alias of :py:class:`int`',
'',
]

View File

@ -265,8 +265,8 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
'.. py:class:: Quux(iterable=(), /)',
' :module: target.classes',
'',
' Bases: :class:`~typing.List`\\ '
'[:obj:`~typing.Union`\\ [:class:`int`, :class:`float`]]',
' Bases: :py:class:`~typing.List`\\ '
'[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]',
'',
' A subclass of List[Union[int, float]]',
'',
@ -296,7 +296,7 @@ def test_autodoc_process_bases(app):
'.. py:class:: Quux(*args, **kwds)',
' :module: target.classes',
'',
' Bases: :class:`int`, :class:`str`',
' Bases: :py:class:`int`, :py:class:`str`',
'',
' A subclass of List[Union[int, float]]',
'',
@ -307,7 +307,7 @@ def test_autodoc_process_bases(app):
'.. py:class:: Quux(iterable=(), /)',
' :module: target.classes',
'',
' Bases: :class:`int`, :class:`str`',
' Bases: :py:class:`int`, :py:class:`str`',
'',
' A subclass of List[Union[int, float]]',
'',
@ -375,7 +375,7 @@ def test_class_alias(app):
'.. py:attribute:: Alias',
' :module: target.classes',
'',
' alias of :class:`target.classes.Foo`',
' alias of :py:class:`target.classes.Foo`',
]

View File

@ -96,7 +96,7 @@ def test_autodata_GenericAlias(app):
'',
' A list of int',
'',
' alias of :class:`~typing.List`\\ [:class:`int`]',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
]
@ -111,7 +111,7 @@ def test_autodata_NewType(app):
'',
' T6',
'',
' alias of :class:`int`',
' alias of :py:class:`int`',
'',
]

View File

@ -41,66 +41,69 @@ class BrokenType:
def test_restify():
assert restify(int) == ":class:`int`"
assert restify(str) == ":class:`str`"
assert restify(None) == ":obj:`None`"
assert restify(Integral) == ":class:`numbers.Integral`"
assert restify(Struct) == ":class:`struct.Struct`"
assert restify(TracebackType) == ":class:`types.TracebackType`"
assert restify(Any) == ":obj:`~typing.Any`"
assert restify(int) == ":py:class:`int`"
assert restify(str) == ":py:class:`str`"
assert restify(None) == ":py:obj:`None`"
assert restify(Integral) == ":py:class:`numbers.Integral`"
assert restify(Struct) == ":py:class:`struct.Struct`"
assert restify(TracebackType) == ":py:class:`types.TracebackType`"
assert restify(Any) == ":py:obj:`~typing.Any`"
def test_restify_type_hints_containers():
assert restify(List) == ":class:`~typing.List`"
assert restify(Dict) == ":class:`~typing.Dict`"
assert restify(List[int]) == ":class:`~typing.List`\\ [:class:`int`]"
assert restify(List[str]) == ":class:`~typing.List`\\ [:class:`str`]"
assert restify(Dict[str, float]) == (":class:`~typing.Dict`\\ "
"[:class:`str`, :class:`float`]")
assert restify(Tuple[str, str, str]) == (":class:`~typing.Tuple`\\ "
"[:class:`str`, :class:`str`, :class:`str`]")
assert restify(Tuple[str, ...]) == ":class:`~typing.Tuple`\\ [:class:`str`, ...]"
assert restify(Tuple[()]) == ":class:`~typing.Tuple`\\ [()]"
assert restify(List[Dict[str, Tuple]]) == (":class:`~typing.List`\\ "
"[:class:`~typing.Dict`\\ "
"[:class:`str`, :class:`~typing.Tuple`]]")
assert restify(MyList[Tuple[int, int]]) == (":class:`tests.test_util_typing.MyList`\\ "
"[:class:`~typing.Tuple`\\ "
"[:class:`int`, :class:`int`]]")
assert restify(Generator[None, None, None]) == (":class:`~typing.Generator`\\ "
"[:obj:`None`, :obj:`None`, :obj:`None`]")
assert restify(List) == ":py:class:`~typing.List`"
assert restify(Dict) == ":py:class:`~typing.Dict`"
assert restify(List[int]) == ":py:class:`~typing.List`\\ [:py:class:`int`]"
assert restify(List[str]) == ":py:class:`~typing.List`\\ [:py:class:`str`]"
assert restify(Dict[str, float]) == (":py:class:`~typing.Dict`\\ "
"[:py:class:`str`, :py:class:`float`]")
assert restify(Tuple[str, str, str]) == (":py:class:`~typing.Tuple`\\ "
"[:py:class:`str`, :py:class:`str`, "
":py:class:`str`]")
assert restify(Tuple[str, ...]) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, ...]"
assert restify(Tuple[()]) == ":py:class:`~typing.Tuple`\\ [()]"
assert restify(List[Dict[str, Tuple]]) == (":py:class:`~typing.List`\\ "
"[:py:class:`~typing.Dict`\\ "
"[:py:class:`str`, :py:class:`~typing.Tuple`]]")
assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util_typing.MyList`\\ "
"[:py:class:`~typing.Tuple`\\ "
"[:py:class:`int`, :py:class:`int`]]")
assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ "
"[:py:obj:`None`, :py:obj:`None`, "
":py:obj:`None`]")
def test_restify_type_hints_Callable():
assert restify(Callable) == ":class:`~typing.Callable`"
assert restify(Callable) == ":py:class:`~typing.Callable`"
if sys.version_info >= (3, 7):
assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ "
"[[:class:`str`], :class:`int`]")
assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ "
"[[...], :class:`int`]")
assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ "
"[[:py:class:`str`], :py:class:`int`]")
assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ "
"[[...], :py:class:`int`]")
else:
assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ "
"[:class:`str`, :class:`int`]")
assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ "
"[..., :class:`int`]")
assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ "
"[:py:class:`str`, :py:class:`int`]")
assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ "
"[..., :py:class:`int`]")
def test_restify_type_hints_Union():
assert restify(Optional[int]) == ":obj:`~typing.Optional`\\ [:class:`int`]"
assert restify(Union[str, None]) == ":obj:`~typing.Optional`\\ [:class:`str`]"
assert restify(Union[int, str]) == ":obj:`~typing.Union`\\ [:class:`int`, :class:`str`]"
assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]"
assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`str`]")
if sys.version_info >= (3, 7):
assert restify(Union[int, Integral]) == (":obj:`~typing.Union`\\ "
"[:class:`int`, :class:`numbers.Integral`]")
assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`numbers.Integral`]")
assert (restify(Union[MyClass1, MyClass2]) ==
(":obj:`~typing.Union`\\ "
"[:class:`tests.test_util_typing.MyClass1`, "
":class:`tests.test_util_typing.<MyClass2>`]"))
(":py:obj:`~typing.Union`\\ "
"[:py:class:`tests.test_util_typing.MyClass1`, "
":py:class:`tests.test_util_typing.<MyClass2>`]"))
else:
assert restify(Union[int, Integral]) == ":class:`numbers.Integral`"
assert restify(Union[MyClass1, MyClass2]) == ":class:`tests.test_util_typing.MyClass1`"
assert restify(Union[int, Integral]) == ":py:class:`numbers.Integral`"
assert restify(Union[MyClass1, MyClass2]) == ":py:class:`tests.test_util_typing.MyClass1`"
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@ -109,58 +112,61 @@ def test_restify_type_hints_typevars():
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
assert restify(T) == ":obj:`tests.test_util_typing.T`"
assert restify(T_co) == ":obj:`tests.test_util_typing.T_co`"
assert restify(T_contra) == ":obj:`tests.test_util_typing.T_contra`"
assert restify(List[T]) == ":class:`~typing.List`\\ [:obj:`tests.test_util_typing.T`]"
assert restify(T) == ":py:obj:`tests.test_util_typing.T`"
assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`"
assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`"
assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
if sys.version_info >= (3, 10):
assert restify(MyInt) == ":class:`tests.test_util_typing.MyInt`"
assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
else:
assert restify(MyInt) == ":class:`MyInt`"
assert restify(MyInt) == ":py:class:`MyInt`"
def test_restify_type_hints_custom_class():
assert restify(MyClass1) == ":class:`tests.test_util_typing.MyClass1`"
assert restify(MyClass2) == ":class:`tests.test_util_typing.<MyClass2>`"
assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`"
assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`"
def test_restify_type_hints_alias():
MyStr = str
MyTuple = Tuple[str, str]
assert restify(MyStr) == ":class:`str`"
assert restify(MyTuple) == ":class:`~typing.Tuple`\\ [:class:`str`, :class:`str`]"
assert restify(MyStr) == ":py:class:`str`"
assert restify(MyTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]"
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
def test_restify_type_ForwardRef():
from typing import ForwardRef # type: ignore
assert restify(ForwardRef("myint")) == ":class:`myint`"
assert restify(ForwardRef("myint")) == ":py:class:`myint`"
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
def test_restify_type_Literal():
from typing import Literal # type: ignore
assert restify(Literal[1, "2", "\r"]) == ":obj:`~typing.Literal`\\ [1, '2', '\\r']"
assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
def test_restify_pep_585():
assert restify(list[str]) == ":class:`list`\\ [:class:`str`]" # type: ignore
assert restify(dict[str, str]) == ":class:`dict`\\ [:class:`str`, :class:`str`]" # type: ignore
assert restify(dict[str, tuple[int, ...]]) == \
":class:`dict`\\ [:class:`str`, :class:`tuple`\\ [:class:`int`, ...]]" # type: ignore
assert restify(list[str]) == ":py:class:`list`\\ [:py:class:`str`]" # type: ignore
assert restify(dict[str, str]) == (":py:class:`dict`\\ " # type: ignore
"[:py:class:`str`, :py:class:`str`]")
assert restify(dict[str, tuple[int, ...]]) == (":py:class:`dict`\\ " # type: ignore
"[:py:class:`str`, :py:class:`tuple`\\ "
"[:py:class:`int`, ...]]")
@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_restify_type_union_operator():
assert restify(int | None) == ":class:`int` | :obj:`None`" # type: ignore
assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore
assert restify(int | str | None) == ":class:`int` | :class:`str` | :obj:`None`" # type: ignore
assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore
assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore
assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore
":py:obj:`None`")
def test_restify_broken_type_hints():
assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'
assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`'
def test_stringify():