Skip to content

Commit

Permalink
Merge pull request #8803 from tk0miya/8775_autodoc_type_union_operator
Browse files Browse the repository at this point in the history
autodoc: Support type union operator (PEP-604) (refs: #8775)
  • Loading branch information
tk0miya committed Feb 1, 2021
2 parents ce7be36 + 7318d1d commit 2956f19
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGES
Expand Up @@ -38,6 +38,7 @@ Features added
info-field-list
* #8514: autodoc: Default values of overloaded functions are taken from actual
implementation if they're ellipsis
* #8775: autodoc: Support type union operator (PEP-604) in Python 3.10 or above
* #8619: html: kbd role generates customizable HTML tags for compound keys
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()`
Expand Down
19 changes: 19 additions & 0 deletions sphinx/util/typing.py
Expand Up @@ -30,6 +30,11 @@ def _evaluate(self, globalns: Dict, localns: Dict) -> Any:
ref = _ForwardRef(self.arg)
return ref._eval_type(globalns, localns)

if sys.version_info > (3, 10):
from types import Union as types_Union
else:
types_Union = None

if False:
# For type annotation
from typing import Type # NOQA # for python3.5.1
Expand Down Expand Up @@ -100,6 +105,12 @@ def restify(cls: Optional["Type"]) -> str:
return ':class:`struct.Struct`'
elif inspect.isNewType(cls):
return ':class:`%s`' % cls.__name__
elif types_Union and isinstance(cls, types_Union):
if len(cls.__args__) > 1 and None in cls.__args__:
args = ' | '.join(restify(a) for a in cls.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(restify(a) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'):
return ':class:`%s`' % cls.__name__
else:
Expand Down Expand Up @@ -336,6 +347,8 @@ def _stringify_py37(annotation: Any) -> str:
elif hasattr(annotation, '__origin__'):
# instantiated generic provided by a user
qualname = stringify(annotation.__origin__)
elif types_Union and isinstance(annotation, types_Union): # types.Union (for py3.10+)
qualname = 'types.Union'
else:
# we weren't able to extract the base type, appending arguments would
# only make them appear twice
Expand All @@ -355,6 +368,12 @@ def _stringify_py37(annotation: Any) -> str:
else:
args = ', '.join(stringify(a) for a in annotation.__args__)
return 'Union[%s]' % args
elif qualname == 'types.Union':
if len(annotation.__args__) > 1 and None in annotation.__args__:
args = ' | '.join(stringify(a) for a in annotation.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(stringify(a) for a in annotation.__args__)
elif qualname == 'Callable':
args = ', '.join(stringify(a) for a in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1])
Expand Down
16 changes: 16 additions & 0 deletions tests/roots/test-ext-autodoc/target/pep604.py
@@ -0,0 +1,16 @@
from __future__ import annotations

attr: int | str #: docstring


def sum(x: int | str, y: int | str) -> int | str:
"""docstring"""


class Foo:
"""docstring"""

attr: int | str #: docstring

def meth(self, x: int | str, y: int | str) -> int | str:
"""docstring"""
44 changes: 44 additions & 0 deletions tests/test_ext_autodoc.py
Expand Up @@ -2237,6 +2237,50 @@ def test_name_mangling(app):
]


@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_type_union_operator(app):
options = {'members': None}
actual = do_autodoc(app, 'module', 'target.pep604', options)
assert list(actual) == [
'',
'.. py:module:: target.pep604',
'',
'',
'.. py:class:: Foo()',
' :module: target.pep604',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr',
' :module: target.pep604',
' :type: int | str',
'',
' docstring',
'',
'',
' .. py:method:: Foo.meth(x: int | str, y: int | str) -> int | str',
' :module: target.pep604',
'',
' docstring',
'',
'',
'.. py:data:: attr',
' :module: target.pep604',
' :type: int | str',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int | str, y: int | str) -> int | str',
' :module: target.pep604',
'',
' docstring',
'',
]


@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_hide_value(app):
Expand Down
14 changes: 14 additions & 0 deletions tests/test_util_typing.py
Expand Up @@ -117,6 +117,13 @@ def test_restify_type_ForwardRef():
assert restify(ForwardRef("myint")) == ":class:`myint`"


@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_restify_type_union_operator():
assert restify(int | None) == "Optional[:class:`int`]" # type: ignore
assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore
assert restify(int | str | None) == "Optional[:class:`int` | :class:`str`]" # type: ignore


def test_restify_broken_type_hints():
assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'

Expand Down Expand Up @@ -206,5 +213,12 @@ def test_stringify_type_hints_alias():
assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore


@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_stringify_type_union_operator():
assert stringify(int | None) == "Optional[int]" # type: ignore
assert stringify(int | str) == "int | str" # type: ignore
assert stringify(int | str | None) == "Optional[int | str]" # type: ignore


def test_stringify_broken_type_hints():
assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType'

0 comments on commit 2956f19

Please sign in to comment.