Skip to content

Commit

Permalink
Fix sphinx-doc#9630: autodoc: Failed to build xrefs if primary_domain…
Browse files Browse the repository at this point in the history
… 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 thme robust.
  • Loading branch information
tk0miya committed Sep 14, 2021
1 parent ba2439a commit 0be261c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 98 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -16,6 +16,9 @@ Features added
Bugs fixed
----------

* #9630: autodoc: Failed to build cross references if :confval:`primary_domain`
is not 'py'

Testing
--------

Expand Down
68 changes: 34 additions & 34 deletions sphinx/util/typing.py
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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__)

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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__
Expand All @@ -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:
Expand Down
134 changes: 70 additions & 64 deletions tests/test_util_typing.py
Expand Up @@ -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.')
Expand All @@ -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():
Expand Down

0 comments on commit 0be261c

Please sign in to comment.