Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA
2020-11-09 02:50:08 +09:00
15 changed files with 490 additions and 54 deletions
+4 -1
View File
@@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
name: [py36, py37, py38]
name: [py36, py37, py38, py39]
include:
- name: py36
python: 3.6
@@ -19,6 +19,9 @@ jobs:
- name: py38
python: 3.8
docutils: du15
- name: py39
python: 3.9
docutils: du16
coverage: "--cov ./ --cov-append --cov-config setup.cfg"
env:
PYTEST_ADDOPTS: ${{ matrix.coverage }}
+14
View File
@@ -58,12 +58,20 @@ Dependencies
Incompatible changes
--------------------
* #8105: autodoc: the signature of class constructor will be shown for decorated
classes, not a signature of decorator
Deprecated
----------
* The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()``
Features added
--------------
* #8119: autodoc: Allow to determine whether a member not included in
``__all__`` attribute of the module should be documented or not via
:event:`autodoc-skip-member` event
* #6914: Add a new event :event:`warn-missing-reference` to custom warning
messages when failed to resolve a cross-reference
* #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference
@@ -72,6 +80,9 @@ Bugs fixed
----------
* #7613: autodoc: autodoc does not respect __signature__ of the class
* #4606: autodoc: the location of the warning is incorrect for inherited method
* #8105: autodoc: the signature of class constructor is incorrect if the class
is decorated
Testing
--------
@@ -94,6 +105,9 @@ Features added
Bugs fixed
----------
* #8219: autodoc: Parameters for generic class are not shown when super class is
a generic class and show-inheritance option is given (in Python 3.7 or above)
Testing
--------
+5
View File
@@ -56,6 +56,11 @@ The following is a list of deprecated interfaces.
- 6.0
- ``docutils.utils.smartyquotes``
* - The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()``
- 3.4
- 5.0
- N/A
* - ``sphinx.builders.latex.LaTeXBuilder.usepackages``
- 3.3
- 5.0
+3 -3
View File
@@ -272,9 +272,9 @@ identifier and put ``sphinx.po`` in there. Don't forget to update the possible
values for :confval:`language` in ``doc/usage/configuration.rst``.
The Sphinx core messages can also be translated on `Transifex
<https://www.transifex.com/sphinx-doc/>`_. There ``tx`` client tool, which is
provided by the ``transifex_client`` Python package, can be used to pull
translations in ``.po`` format from Transifex. To do this, go to
<https://www.transifex.com/sphinx-doc/sphinx-1/>`_. There ``tx`` client tool,
which is provided by the ``transifex_client`` Python package, can be used to
pull translations in ``.po`` format from Transifex. To do this, go to
``sphinx/locale`` and then run ``tx pull -f -l LANG`` where ``LANG`` is an
existing language identifier. It is good practice to run ``python setup.py
update_catalog`` afterwards to make sure the ``.po`` file has the canonical
+83 -34
View File
@@ -37,7 +37,7 @@ from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.inspect import (
evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature
)
from sphinx.util.typing import stringify as stringify_typehint
from sphinx.util.typing import restify, stringify as stringify_typehint
if TYPE_CHECKING:
from sphinx.ext.autodoc.directive import DocumenterBridge
@@ -256,6 +256,32 @@ class Options(dict):
return None
class ObjectMember(tuple):
"""A member of object.
This is used for the result of `Documenter.get_object_members()` to
represent each member of the object.
.. Note::
An instance of this class behaves as a tuple of (name, object)
for compatibility to old Sphinx. The behavior will be dropped
in the future. Therefore extensions should not use the tuple
interface.
"""
def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any:
return super().__new__(cls, (name, obj)) # type: ignore
def __init__(self, name: str, obj: Any, skipped: bool = False) -> None:
self.__name__ = name
self.object = obj
self.skipped = skipped
ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]]
class Documenter:
"""
A Documenter knows how to autodocument a single object type. When
@@ -537,9 +563,18 @@ class Documenter:
yield from docstringlines
def get_sourcename(self) -> str:
if (getattr(self.object, '__module__', None) and
getattr(self.object, '__qualname__', None)):
# Get the correct location of docstring from self.object
# to support inherited methods
fullname = '%s.%s' % (self.object.__module__, self.object.__qualname__)
else:
fullname = self.fullname
if self.analyzer:
return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname)
return 'docstring of %s' % self.fullname
return '%s:docstring of %s' % (self.analyzer.srcname, fullname)
else:
return 'docstring of %s' % fullname
def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
"""Add content from docstrings, attribute documentation and user."""
@@ -574,7 +609,7 @@ class Documenter:
for line, src in zip(more_content.data, more_content.items):
self.add_line(line, src[0], src[1])
def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]:
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
"""Return `(members_check_module, members)` where `members` is a
list of `(membername, member)` pairs of the members of *self.object*.
@@ -584,10 +619,10 @@ class Documenter:
members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer)
if not want_all:
if not self.options.members:
return False, []
return False, [] # type: ignore
# specific members given
selected = []
for name in self.options.members:
for name in self.options.members: # type: str
if name in members:
selected.append((name, members[name].value))
else:
@@ -600,7 +635,7 @@ class Documenter:
return False, [(m.name, m.value) for m in members.values()
if m.directly_defined]
def filter_members(self, members: List[Tuple[str, Any]], want_all: bool
def filter_members(self, members: ObjectMembers, want_all: bool
) -> List[Tuple[str, Any, bool]]:
"""Filter the given member list.
@@ -639,7 +674,8 @@ class Documenter:
attr_docs = {}
# process members and determine which to skip
for (membername, member) in members:
for obj in members:
membername, member = obj
# if isattr is True, the member is documented as an attribute
if member is INSTANCEATTR:
isattr = True
@@ -716,6 +752,10 @@ class Documenter:
# ignore undocumented members if :undoc-members: is not given
keep = has_doc or self.options.undoc_members
if isinstance(obj, ObjectMember) and obj.skipped:
# forcedly skipped member (ex. a module attribute not defined in __all__)
keep = False
# give the user a chance to decide whether this member
# should be skipped
if self.env.app:
@@ -977,28 +1017,35 @@ class ModuleDocumenter(Documenter):
if self.options.deprecated:
self.add_line(' :deprecated:', sourcename)
def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]:
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
if want_all:
if self.__all__:
memberlist = self.__all__
else:
members = get_module_members(self.object)
if not self.__all__:
# for implicit module members, check __module__ to avoid
# documenting imported objects
return True, get_module_members(self.object)
return True, members
else:
ret = []
for name, value in members:
if name in self.__all__:
ret.append(ObjectMember(name, value))
else:
ret.append(ObjectMember(name, value, skipped=True))
return False, ret
else:
memberlist = self.options.members or []
ret = []
for mname in memberlist:
try:
ret.append((mname, safe_getattr(self.object, mname)))
except AttributeError:
logger.warning(
__('missing attribute mentioned in :members: or __all__: '
'module %s, attribute %s') %
(safe_getattr(self.object, '__name__', '???'), mname),
type='autodoc'
)
return False, ret
ret = []
for name in memberlist:
try:
value = safe_getattr(self.object, name)
ret.append(ObjectMember(name, value))
except AttributeError:
logger.warning(__('missing attribute mentioned in :members: option: '
'module %s, attribute %s') %
(safe_getattr(self.object, '__name__', '???'), name),
type='autodoc')
return False, ret
def sort_members(self, documenters: List[Tuple["Documenter", bool]],
order: str) -> List[Tuple["Documenter", bool]]:
@@ -1198,7 +1245,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
try:
self.env.app.emit('autodoc-before-process-signature', self.object, False)
sig = inspect.signature(self.object, follow_wrapped=True,
sig = inspect.signature(self.object,
type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
@@ -1512,13 +1559,16 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if not self.doc_as_attr and self.options.show_inheritance:
sourcename = self.get_sourcename()
self.add_line('', sourcename)
if hasattr(self.object, '__bases__') and len(self.object.__bases__):
bases = [':class:`%s`' % b.__name__
if b.__module__ in ('__builtin__', 'builtins')
else ':class:`%s.%s`' % (b.__module__, b.__qualname__)
for b in self.object.__bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases),
sourcename)
if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__):
# A subclass of generic types
# refs: PEP-560 <https://www.python.org/dev/peps/pep-0560/>
bases = [restify(cls) for cls in self.object.__orig_bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
elif hasattr(self.object, '__bases__') and len(self.object.__bases__):
# A normal class
bases = [restify(cls) for cls in self.object.__bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
def get_doc(self, ignore: int = None) -> List[List[str]]:
lines = getattr(self, '_new_docstrings', None)
@@ -1831,7 +1881,6 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
else:
self.env.app.emit('autodoc-before-process-signature', self.object, True)
sig = inspect.signature(self.object, bound_method=True,
follow_wrapped=True,
type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
+11 -10
View File
@@ -62,14 +62,6 @@ def getargspec(func: Callable) -> Any:
methods."""
warnings.warn('sphinx.ext.inspect.getargspec() is deprecated',
RemovedInSphinx50Warning, stacklevel=2)
# On 3.5+, signature(int) or similar raises ValueError. On 3.4, it
# succeeds with a bogus signature. We want a TypeError uniformly, to
# match historical behavior.
if (isinstance(func, type) and
is_builtin_class_method(func, "__new__") and
is_builtin_class_method(func, "__init__")):
raise TypeError(
"can't compute signature for built-in type {}".format(func))
sig = inspect.signature(func)
@@ -320,6 +312,9 @@ def isgenericalias(obj: Any) -> bool:
elif (hasattr(types, 'GenericAlias') and # only for py39+
isinstance(obj, types.GenericAlias)): # type: ignore
return True
elif (hasattr(typing, '_SpecialGenericAlias') and # for py39+
isinstance(obj, typing._SpecialGenericAlias)): # type: ignore
return True
else:
return False
@@ -422,14 +417,20 @@ def _should_unwrap(subject: Callable) -> bool:
return False
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False,
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = None,
type_aliases: Dict = {}) -> inspect.Signature:
"""Return a Signature object for the given *subject*.
:param bound_method: Specify *subject* is a bound method or not
:param follow_wrapped: Same as ``inspect.signature()``.
Defaults to ``False`` (get a signature of *subject*).
"""
if follow_wrapped is None:
follow_wrapped = True
else:
warnings.warn('The follow_wrapped argument of sphinx.util.inspect.signature() is '
'deprecated', RemovedInSphinx50Warning, stacklevel=2)
try:
try:
if _should_unwrap(subject):
+194 -1
View File
@@ -10,7 +10,7 @@
import sys
import typing
from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union
from docutils import nodes
from docutils.parsers.rst.states import Inliner
@@ -30,6 +30,10 @@ else:
ref = _ForwardRef(self.arg)
return ref._eval_type(globalns, localns)
if False:
# For type annotation
from typing import Type # NOQA # for python3.5.1
# An entry of Directive.option_spec
DirectiveOption = Callable[[str], Any]
@@ -60,6 +64,195 @@ 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."""
if cls is None or cls is NoneType:
return ':obj:`None`'
elif cls is Ellipsis:
return '...'
elif cls.__module__ in ('__builtin__', 'builtins'):
return ':class:`%s`' % cls.__name__
else:
if sys.version_info >= (3, 7): # py37+
return _restify_py37(cls)
else:
return _restify_py36(cls)
def _restify_py37(cls: Optional["Type"]) -> str:
"""Convert python class to a reST reference."""
from sphinx.util import inspect # lazy loading
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])
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % args
else:
return ':obj:`Optional`\\ [%s]' % restify(cls.__args__[0])
else:
args = ', '.join(restify(a) for a in cls.__args__)
return ':obj:`Union`\\ [%s]' % args
elif inspect.isgenericalias(cls):
if getattr(cls, '_name', None):
if cls.__module__ == 'typing':
text = ':class:`%s`' % cls._name
else:
text = ':class:`%s.%s`' % (cls.__module__, cls._name)
else:
text = restify(cls.__origin__)
if not hasattr(cls, '__args__'):
pass
elif all(is_system_TypeVar(a) for a in cls.__args__):
# 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]))
elif cls.__args__:
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
return text
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':class:`%s`' % cls.__qualname__
else:
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
return ':obj:`%s`' % cls._name
else:
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
else:
# not a class (ex. TypeVar)
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
def _restify_py36(cls: Optional["Type"]) -> str:
module = getattr(cls, '__module__', None)
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.', '')
elif hasattr(cls, '__qualname__'):
qualname = '%s.%s' % (module, cls.__qualname__)
else:
qualname = repr(cls)
if (isinstance(cls, typing.TupleMeta) and # type: ignore
not hasattr(cls, '__tuple_params__')): # for Python 3.6
params = cls.__args__
if params:
param_str = ', '.join(restify(p) for p in params)
return ':class:`%s`\\ [%s]' % (qualname, param_str)
else:
return ':class:`%s`' % qualname
elif isinstance(cls, typing.GenericMeta):
params = None
if hasattr(cls, '__args__'):
# for Python 3.5.2+
if cls.__args__ is None or len(cls.__args__) <= 2: # type: ignore # NOQA
params = cls.__args__ # type: ignore
elif cls.__origin__ == Generator: # type: ignore
params = cls.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(restify(arg) for arg in cls.__args__[:-1]) # type: ignore
result = restify(cls.__args__[-1]) # type: ignore
return ':class:`%s`\\ [[%s], %s]' % (qualname, args, result)
elif hasattr(cls, '__parameters__'):
# for Python 3.5.0 and 3.5.1
params = cls.__parameters__ # type: ignore
if params:
param_str = ', '.join(restify(p) for p in params)
return ':class:`%s`\\ [%s]' % (qualname, param_str)
else:
return ':class:`%s`' % qualname
elif (hasattr(typing, 'UnionMeta') and
isinstance(cls, typing.UnionMeta) and # type: ignore
hasattr(cls, '__union_params__')): # for Python 3.5
params = cls.__union_params__
if params is not None:
if len(params) == 2 and params[1] is NoneType:
return ':obj:`Optional`\\ [%s]' % restify(params[0])
else:
param_str = ', '.join(restify(p) for p in params)
return ':obj:`%s`\\ [%s]' % (qualname, param_str)
else:
return ':obj:`%s`' % qualname
elif (hasattr(cls, '__origin__') and
cls.__origin__ is typing.Union): # for Python 3.5.2+
params = cls.__args__
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])
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % param_str
else:
return ':obj:`Optional`\\ [%s]' % restify(params[0])
else:
param_str = ', '.join(restify(p) for p in params)
return ':obj:`Union`\\ [%s]' % param_str
else:
return ':obj:`Union`'
elif (isinstance(cls, typing.CallableMeta) and # type: ignore
getattr(cls, '__args__', None) is not None and
hasattr(cls, '__result__')): # for Python 3.5
# Skipped in the case of plain typing.Callable
args = cls.__args__
if args is None:
return qualname
elif args is Ellipsis:
args_str = '...'
else:
formatted_args = (restify(a) for a in args) # type: ignore
args_str = '[%s]' % ', '.join(formatted_args)
return ':class:`%s`\\ [%s, %s]' % (qualname, args_str, stringify(cls.__result__))
elif (isinstance(cls, typing.TupleMeta) and # type: ignore
hasattr(cls, '__tuple_params__') and
hasattr(cls, '__tuple_use_ellipsis__')): # for Python 3.5
params = cls.__tuple_params__
if params is not None:
param_strings = [restify(p) for p in params]
if cls.__tuple_use_ellipsis__:
param_strings.append('...')
return ':class:`%s`\\ [%s]' % (qualname, ', '.join(param_strings))
else:
return ':class:`%s`' % qualname
elif hasattr(cls, '__qualname__'):
if cls.__module__ == 'typing':
return ':class:`%s`' % cls.__qualname__
else:
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
return ':obj:`%s`' % cls._name
else:
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
elif hasattr(cls, '__name__'):
# not a class (ex. TypeVar)
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
else:
# others (ex. Any)
if cls.__module__ == 'typing':
return ':obj:`%s`' % qualname
else:
return ':obj:`%s.%s`' % (cls.__module__, qualname)
def stringify(annotation: Any) -> str:
"""Stringify type annotation object."""
if isinstance(annotation, str):
@@ -1,4 +1,5 @@
from inspect import Parameter, Signature
from typing import List, Union
class Foo:
@@ -21,3 +22,8 @@ class Qux:
def __init__(self, x, y):
pass
class Quux(List[Union[int, float]]):
"""A subclass of List[Union[int, float]]"""
pass
@@ -29,3 +29,25 @@ class Bar:
@deco1
def meth(self, name=None, age=None):
pass
class Baz:
@deco1
def __init__(self, name=None, age=None):
pass
class Qux:
@deco1
def __new__(self, name=None, age=None):
pass
class _Metaclass(type):
@deco1
def __call__(self, name=None, age=None):
pass
class Quux(metaclass=_Metaclass):
pass
@@ -1,4 +1,4 @@
from typing import Tuple, Union
from typing import Any, Tuple, Union
def incr(a: int, b: int = 1) -> int:
@@ -11,7 +11,7 @@ def decr(a, b = 1):
class Math:
def __init__(self, s: str, o: object = None) -> None:
def __init__(self, s: str, o: Any = None) -> None:
pass
def incr(self, a: int, b: int = 1) -> int:
+44
View File
@@ -9,6 +9,8 @@
:license: BSD, see LICENSE for details.
"""
import sys
import pytest
from test_ext_autodoc import do_autodoc
@@ -48,3 +50,45 @@ def test_classes(app):
'',
]
def test_decorators(app):
actual = do_autodoc(app, 'class', 'target.decorator.Baz')
assert list(actual) == [
'',
'.. py:class:: Baz(name=None, age=None)',
' :module: target.decorator',
'',
]
actual = do_autodoc(app, 'class', 'target.decorator.Qux')
assert list(actual) == [
'',
'.. py:class:: Qux(name=None, age=None)',
' :module: target.decorator',
'',
]
actual = do_autodoc(app, 'class', 'target.decorator.Quux')
assert list(actual) == [
'',
'.. py:class:: Quux(name=None, age=None)',
' :module: target.decorator',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_show_inheritance_for_subclass_of_generic_type(app):
options = {'show-inheritance': True}
actual = do_autodoc(app, 'class', 'target.classes.Quux', options)
assert list(actual) == [
'',
'.. py:class:: Quux(iterable=(), /)',
' :module: target.classes',
'',
' Bases: :class:`List`\\ [:obj:`Union`\\ [:class:`int`, :class:`float`]]',
'',
' A subclass of List[Union[int, float]]',
'',
]
+1 -1
View File
@@ -490,7 +490,7 @@ def test_autodoc_typehints_signature(app):
'.. py:module:: target.typehints',
'',
'',
'.. py:class:: Math(s: str, o: object = None)',
'.. py:class:: Math(s: str, o: Any = None)',
' :module: target.typehints',
'',
'',
+25
View File
@@ -80,3 +80,28 @@ def test_between_exclude(app):
' third line',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_skip_module_member(app):
def autodoc_skip_member(app, what, name, obj, skip, options):
if name == "Class":
return True # Skip "Class" class in __all__
elif name == "raises":
return False # Show "raises()" function (not in __all__)
app.connect('autodoc-skip-member', autodoc_skip_member)
options = {"members": None}
actual = do_autodoc(app, 'module', 'target', options)
assert list(actual) == [
'',
'.. py:module:: target',
'',
'',
'.. py:function:: raises(exc, func, *args, **kwds)',
' :module: target',
'',
' Raise AssertionError if ``func(*args, **kwds)`` does not raise *exc*.',
'',
]
+1 -1
View File
@@ -100,7 +100,7 @@ def test_signature_methods():
# wrapped bound method
sig = inspect.signature(wrapped_bound_method)
assert stringify_signature(sig) == '(*args, **kwargs)'
assert stringify_signature(sig) == '(arg1, **kwargs)'
def test_signature_partialmethod():
+75 -1
View File
@@ -16,7 +16,7 @@ from typing import (
import pytest
from sphinx.util.typing import stringify
from sphinx.util.typing import restify, stringify
class MyClass1:
@@ -36,6 +36,80 @@ class BrokenType:
__args__ = int
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(Any) == ":obj:`Any`"
def test_restify_type_hints_containers():
assert restify(List) == ":class:`List`"
assert restify(Dict) == ":class:`Dict`"
assert restify(List[int]) == ":class:`List`\\ [:class:`int`]"
assert restify(List[str]) == ":class:`List`\\ [:class:`str`]"
assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]"
assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]"
assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]"
assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]"
assert restify(MyList[Tuple[int, int]]) == ":class:`test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]"
assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]"
def test_restify_type_hints_Callable():
assert restify(Callable) == ":class:`Callable`"
if sys.version_info >= (3, 7):
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]"
assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]"
else:
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]"
assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]"
def test_restify_type_hints_Union():
assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]"
assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]"
assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]"
if sys.version_info >= (3, 7):
assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]"
assert (restify(Union[MyClass1, MyClass2]) ==
":obj:`Union`\\ [:class:`test_util_typing.MyClass1`, :class:`test_util_typing.<MyClass2>`]")
else:
assert restify(Union[int, Integral]) == ":class:`numbers.Integral`"
assert restify(Union[MyClass1, MyClass2]) == ":class:`test_util_typing.MyClass1`"
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
def test_restify_type_hints_typevars():
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
assert restify(T) == ":obj:`test_util_typing.T`"
assert restify(T_co) == ":obj:`test_util_typing.T_co`"
assert restify(T_contra) == ":obj:`test_util_typing.T_contra`"
assert restify(List[T]) == ":class:`List`\\ [:obj:`test_util_typing.T`]"
def test_restify_type_hints_custom_class():
assert restify(MyClass1) == ":class:`test_util_typing.MyClass1`"
assert restify(MyClass2) == ":class:`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:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore
def test_restify_broken_type_hints():
assert restify(BrokenType) == ':class:`test_util_typing.BrokenType`'
def test_stringify():
assert stringify(int) == "int"
assert stringify(str) == "str"