Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 103 additions & 19 deletions sphinx/util/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ def _evaluate(self, globalns: Dict, localns: Dict) -> Any:


def get_type_hints(obj: Any, globalns: Dict = None, localns: Dict = None) -> Dict[str, Any]:
"""Return a dictionary containing type hints for a function, method, module or class object.
"""Return collected type hints for a callable, module, or class.

This is a simple wrapper of `typing.get_type_hints()` that does not raise an error on
runtime.
This is a simple wrapper of ``typing.get_type_hints()`` that does not raise an error
at runtime.
"""
from sphinx.util.inspect import safe_getattr # lazy loading

Expand Down Expand Up @@ -192,10 +192,40 @@ def _restify_py37(cls: Optional[Type]) -> str:
elif isinstance(cls, typing._SpecialForm):
return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name)
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
module = getattr(cls, '__module__', None)

def _normalized(value: Any) -> Optional[str]:
if value is None:
return None
text = value if isinstance(value, str) else str(value)
text = text.strip()
if not text:
return None
if module and text.startswith(module + '.'):
text = text[len(module) + 1:]
if text.endswith('.__name__'):
text = text[: -len('.__name__')]
elif text.endswith('.__qualname__'):
text = text[: -len('.__qualname__')]
return text

qualname = _normalized(getattr(cls, '__qualname__', None))
if not qualname:
qualname = _normalized(getattr(cls, '__name__', None))

if qualname:
if module == 'typing':
return ':py:class:`~%s.%s`' % (module, qualname)
elif module:
return ':py:class:`%s.%s`' % (module, qualname)
else:
return ':py:class:`%s`' % qualname
fallback = _normalized(getattr(cls, '__name__', None))
if fallback:
if module:
return ':py:class:`%s.%s`' % (module, fallback)
return ':py:class:`%s`' % fallback
return repr(cls)
elif isinstance(cls, ForwardRef):
return ':py:class:`%s`' % cls.__forward_arg__
else:
Expand All @@ -208,19 +238,58 @@ def _restify_py37(cls: Optional[Type]) -> str:

def _restify_py36(cls: Optional[Type]) -> str:
module = getattr(cls, '__module__', None)

def _normalized(value: Any) -> Optional[str]:
if value is None:
return None
text = value if isinstance(value, str) else str(value)
text = text.strip()
if not text:
return None
if module and text.startswith(module + '.'):
text = text[len(module) + 1:]
if text.endswith('.__name__'):
text = text[: -len('.__name__')]
elif text.endswith('.__qualname__'):
text = text[: -len('.__qualname__')]
return text

if module == 'typing':
if getattr(cls, '_name', None):
qualname = cls._name
elif getattr(cls, '__qualname__', None):
qualname = cls.__qualname__
elif getattr(cls, '__forward_arg__', None):
qualname = cls.__forward_arg__
elif getattr(cls, '__origin__', None):
qualname = stringify(cls.__origin__) # ex. Union
else:
qualname = repr(cls).replace('typing.', '')
qualname = _normalized(getattr(cls, '__qualname__', None))
if not qualname:
qualname = _normalized(getattr(cls, '__name__', None))

if not qualname and getattr(cls, '__forward_arg__', None):
qualname = cls.__forward_arg__
elif not qualname and getattr(cls, '__origin__', None):
qualname = stringify(cls.__origin__) # ex. Union
elif not qualname:
qualname = repr(cls).replace('typing.', '')
elif hasattr(cls, '__qualname__'):
qualname = '%s.%s' % (module, cls.__qualname__)
qual = _normalized(getattr(cls, '__qualname__', None))
if qual:
qualname = '%s.%s' % (module, qual) if module else qual
else:
fallback = _normalized(getattr(cls, '__name__', None))
if fallback:
qualname = '%s.%s' % (module, fallback) if module else fallback
else:
qualname = repr(cls)
elif getattr(cls, '__forward_arg__', None):
qualname = cls.__forward_arg__
elif getattr(cls, '__origin__', None):
qualname = stringify(cls.__origin__)
elif hasattr(cls, '_name') and getattr(cls, '_name'):
qualname = getattr(cls, '_name')
elif hasattr(cls, '__name__'):
qual = _normalized(getattr(cls, '__name__', None))
if qual:
qualname = '%s.%s' % (module, qual) if module else qual
else:
qualname = repr(cls)
else:
qualname = repr(cls)

Expand Down Expand Up @@ -274,10 +343,25 @@ def _restify_py36(cls: Optional[Type]) -> str:
else:
return ':py:obj:`Union`'
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
qual = _normalized(getattr(cls, '__qualname__', None))
if not qual:
qual = _normalized(getattr(cls, '__name__', None))

if qual:
if module == 'typing':
return ':py:class:`~%s.%s`' % (module, qual)
elif module:
return ':py:class:`%s.%s`' % (module, qual)
else:
return ':py:class:`%s`' % qual

fallback = _normalized(getattr(cls, '__name__', None))
if fallback:
if module:
return ':py:class:`%s.%s`' % (module, fallback)
return ':py:class:`%s`' % fallback

return repr(cls)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
Expand Down
12 changes: 12 additions & 0 deletions tests/roots/test-autodoc-mocked-bases/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os
import sys

sys.path.insert(0, os.path.abspath('.'))

extensions = ['sphinx.ext.autodoc']

source_suffix = '.rst'

autodoc_mock_imports = ['torch']

nitpicky = True
6 changes: 6 additions & 0 deletions tests/roots/test-autodoc-mocked-bases/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Mocked Bases
============

.. automodule:: mocked
:members:
:show-inheritance:
7 changes: 7 additions & 0 deletions tests/roots/test-autodoc-mocked-bases/mocked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import torch.nn as nn


class ModuleSubclass(nn.Module):
"""Docstring for a mocked torch module subclass."""

pass
9 changes: 9 additions & 0 deletions tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,15 @@ def test_autodoc_classmethod(app):
]


@pytest.mark.sphinx('html', testroot='autodoc-mocked-bases')
def test_autodoc_mocked_bases_rendering(app):
options = {'show-inheritance': None}

actual = do_autodoc(app, 'class', 'mocked.ModuleSubclass', options)

assert ' Bases: :py:class:`torch.nn.Module`' in list(actual)


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_staticmethod(app):
actual = do_autodoc(app, 'method', 'target.inheritance.Base.inheritedstaticmeth')
Expand Down
36 changes: 35 additions & 1 deletion tests/test_util_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import sys
import typing
from numbers import Integral
from struct import Struct
from types import TracebackType
Expand All @@ -17,7 +18,7 @@

import pytest

from sphinx.util.typing import restify, stringify
from sphinx.util.typing import _restify_py36, restify, stringify


class MyClass1:
Expand Down Expand Up @@ -129,6 +130,39 @@ def test_restify_type_hints_custom_class():
assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`"


def test_restify_mocked_class_falls_back_to_name():
class MockModule:
pass

MockModule.__module__ = 'torch.nn'
MockModule.__name__ = 'Module'
MockModule.__qualname__ = ''

assert restify(MockModule) == ":py:class:`torch.nn.Module`"

MockModule.__qualname__ = 'Module'
assert restify(MockModule) == ":py:class:`torch.nn.Module`"


def test_restify_py36_mocked_class_falls_back_to_name(monkeypatch):
if not hasattr(typing, 'TupleMeta'):
monkeypatch.setattr(typing, 'TupleMeta', type('TupleMeta', (type,), {}), raising=False)
if not hasattr(typing, 'GenericMeta'):
monkeypatch.setattr(typing, 'GenericMeta', type('GenericMeta', (type,), {}), raising=False)

class MockModule:
pass

MockModule.__module__ = 'torch.nn'
MockModule.__name__ = 'Module'
MockModule.__qualname__ = ''

assert _restify_py36(MockModule) == ":py:class:`torch.nn.Module`"

MockModule.__qualname__ = 'Module'
assert _restify_py36(MockModule) == ":py:class:`torch.nn.Module`"


def test_restify_type_hints_alias():
MyStr = str
MyTuple = Tuple[str, str]
Expand Down