Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autodoc: Support type union operator (PEP-604) (refs: #8775) #8803

Merged
merged 1 commit into from Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'