Add sphinx.util.typing:stringify() to represent annotations as string

This commit is contained in:
Takeshi KOMIYA 2019-07-14 01:54:07 +09:00
parent 2f7823e1a6
commit 05daa3c7ce
5 changed files with 289 additions and 156 deletions

View File

@ -21,6 +21,9 @@ Deprecated
* ``sphinx.roles.Index``
* ``sphinx.util.detect_encoding()``
* ``sphinx.util.get_module_source()``
* ``sphinx.util.inspect.Signature.format_annotation()``
* ``sphinx.util.inspect.Signature.format_annotation_new()``
* ``sphinx.util.inspect.Signature.format_annotation_old()``
Features added
--------------

View File

@ -81,6 +81,21 @@ The following is a list of deprecated interfaces.
- 4.0
- N/A
* - ``sphinx.util.inspect.Signature.format_annotation()``
- 2.4
- 4.0
- ``sphinx.util.typing.stringify()``
* - ``sphinx.util.inspect.Signature.format_annotation_new()``
- 2.4
- 4.0
- ``sphinx.util.typing.stringify()``
* - ``sphinx.util.inspect.Signature.format_annotation_old()``
- 2.4
- 4.0
- ``sphinx.util.typing.stringify()``
* - ``sphinx.builders.gettext.POHEADER``
- 2.3
- 4.0

View File

@ -22,9 +22,9 @@ from inspect import ( # NOQA
from io import StringIO
from typing import Any, Callable, Mapping, List, Tuple
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.util import logging
from sphinx.util.typing import NoneType
from sphinx.util.typing import stringify as stringify_annotation
if sys.version_info > (3, 7):
from types import (
@ -403,11 +403,11 @@ class Signature:
return None
def format_args(self, show_annotation: bool = True) -> str:
def format_param_annotation(param: inspect.Parameter) -> str:
def get_annotation(param: inspect.Parameter) -> Any:
if isinstance(param.annotation, str) and param.name in self.annotations:
return self.format_annotation(self.annotations[param.name])
return self.annotations[param.name]
else:
return self.format_annotation(param.annotation)
return param.annotation
args = []
last_kind = None
@ -431,7 +431,7 @@ class Signature:
arg.write(param.name)
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(format_param_annotation(param))
arg.write(stringify_annotation(get_annotation(param)))
if param.default is not param.empty:
if param.annotation is param.empty or show_annotation is False:
arg.write('=')
@ -444,13 +444,13 @@ class Signature:
arg.write(param.name)
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(format_param_annotation(param))
arg.write(stringify_annotation(get_annotation(param)))
elif param.kind == param.VAR_KEYWORD:
arg.write('**')
arg.write(param.name)
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(format_param_annotation(param))
arg.write(stringify_annotation(get_annotation(param)))
args.append(arg.getvalue())
last_kind = param.kind
@ -459,164 +459,29 @@ class Signature:
return '(%s)' % ', '.join(args)
else:
if 'return' in self.annotations:
annotation = self.format_annotation(self.annotations['return'])
annotation = stringify_annotation(self.annotations['return'])
else:
annotation = self.format_annotation(self.return_annotation)
annotation = stringify_annotation(self.return_annotation)
return '(%s) -> %s' % (', '.join(args), annotation)
def format_annotation(self, annotation: Any) -> str:
"""Return formatted representation of a type annotation.
Show qualified names for types and additional details for types from
the ``typing`` module.
Displaying complex types from ``typing`` relies on its private API.
"""
if isinstance(annotation, str):
return annotation
elif isinstance(annotation, typing.TypeVar): # type: ignore
return annotation.__name__
elif not annotation:
return repr(annotation)
elif annotation is NoneType: # type: ignore
return 'None'
elif getattr(annotation, '__module__', None) == 'builtins':
return annotation.__qualname__
elif annotation is Ellipsis:
return '...'
if sys.version_info >= (3, 7): # py37+
return self.format_annotation_new(annotation)
else:
return self.format_annotation_old(annotation)
"""Return formatted representation of a type annotation."""
warnings.warn('format_annotation() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation)
def format_annotation_new(self, annotation: Any) -> str:
"""format_annotation() for py37+"""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
else:
qualname = self.format_annotation(annotation.__origin__) # ex. Union
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if getattr(annotation, '__args__', None):
if qualname == 'Union':
if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA
return 'Optional[%s]' % self.format_annotation(annotation.__args__[0])
else:
args = ', '.join(self.format_annotation(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
elif qualname == 'Callable':
args = ', '.join(self.format_annotation(a) for a in annotation.__args__[:-1])
returns = self.format_annotation(annotation.__args__[-1])
return '%s[[%s], %s]' % (qualname, args, returns)
elif annotation._special:
return qualname
else:
args = ', '.join(self.format_annotation(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
return qualname
warnings.warn('format_annotation_new() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation)
def format_annotation_old(self, annotation: Any) -> str:
"""format_annotation() for py36 or below"""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
elif getattr(annotation, '__origin__', None):
qualname = self.format_annotation(annotation.__origin__) # ex. Union
else:
qualname = repr(annotation).replace('typing.', '')
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if (isinstance(annotation, typing.TupleMeta) and # type: ignore
not hasattr(annotation, '__tuple_params__')): # for Python 3.6
params = annotation.__args__
if params:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualname, param_str)
else:
return qualname
elif isinstance(annotation, typing.GenericMeta):
params = None
if hasattr(annotation, '__args__'):
# for Python 3.5.2+
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
params = annotation.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(self.format_annotation(arg) for arg
in annotation.__args__[:-1]) # type: ignore
result = self.format_annotation(annotation.__args__[-1]) # type: ignore
return '%s[[%s], %s]' % (qualname, args, result)
elif hasattr(annotation, '__parameters__'):
# for Python 3.5.0 and 3.5.1
params = annotation.__parameters__ # type: ignore
if params is not None:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(typing, 'UnionMeta') and
isinstance(annotation, typing.UnionMeta) and # type: ignore
hasattr(annotation, '__union_params__')): # for Python 3.5
params = annotation.__union_params__
if params is not None:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % self.format_annotation(params[0])
else:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(annotation, '__origin__') and
annotation.__origin__ is typing.Union): # for Python 3.5.2+
params = annotation.__args__
if params is not None:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % self.format_annotation(params[0])
else:
param_str = ', '.join(self.format_annotation(p) for p in params)
return 'Union[%s]' % param_str
elif (isinstance(annotation, typing.CallableMeta) and # type: ignore
getattr(annotation, '__args__', None) is not None and
hasattr(annotation, '__result__')): # for Python 3.5
# Skipped in the case of plain typing.Callable
args = annotation.__args__
if args is None:
return qualname
elif args is Ellipsis:
args_str = '...'
else:
formatted_args = (self.format_annotation(a) for a in args)
args_str = '[%s]' % ', '.join(formatted_args)
return '%s[%s, %s]' % (qualname,
args_str,
self.format_annotation(annotation.__result__))
elif (isinstance(annotation, typing.TupleMeta) and # type: ignore
hasattr(annotation, '__tuple_params__') and
hasattr(annotation, '__tuple_use_ellipsis__')): # for Python 3.5
params = annotation.__tuple_params__
if params is not None:
param_strings = [self.format_annotation(p) for p in params]
if annotation.__tuple_use_ellipsis__:
param_strings.append('...')
return '%s[%s]' % (qualname,
', '.join(param_strings))
return qualname
warnings.warn('format_annotation_old() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation)
def getdoc(obj: Any, attrgetter: Callable = safe_getattr,

View File

@ -8,7 +8,9 @@
:license: BSD, see LICENSE for details.
"""
from typing import Any, Callable, Dict, List, Tuple, Union
import sys
import typing
from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union
from docutils import nodes
from docutils.parsers.rst.states import Inliner
@ -35,3 +37,153 @@ TitleGetter = Callable[[nodes.Node], str]
# inventory data on memory
Inventory = Dict[str, Dict[str, Tuple[str, str, str, str]]]
def stringify(annotation: Any) -> str:
"""Stringify type annotation object."""
if isinstance(annotation, str):
return annotation
elif isinstance(annotation, TypeVar): # type: ignore
return annotation.__name__
elif not annotation:
return repr(annotation)
elif annotation is NoneType: # type: ignore
return 'None'
elif getattr(annotation, '__module__', None) == 'builtins':
return annotation.__qualname__
elif annotation is Ellipsis:
return '...'
if sys.version_info >= (3, 7): # py37+
return _stringify_py37(annotation)
else:
return _stringify_py36(annotation)
def _stringify_py37(annotation: Any) -> str:
"""stringify() for py37+."""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
else:
qualname = stringify(annotation.__origin__) # ex. Union
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if getattr(annotation, '__args__', None):
if qualname == 'Union':
if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA
return 'Optional[%s]' % stringify(annotation.__args__[0])
else:
args = ', '.join(stringify(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
elif qualname == 'Callable':
args = ', '.join(stringify(a) for a in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1])
return '%s[[%s], %s]' % (qualname, args, returns)
elif annotation._special:
return qualname
else:
args = ', '.join(stringify(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
return qualname
def _stringify_py36(annotation: Any) -> str:
"""stringify() for py35 and py36."""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
elif getattr(annotation, '__origin__', None):
qualname = stringify(annotation.__origin__) # ex. Union
else:
qualname = repr(annotation).replace('typing.', '')
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if (isinstance(annotation, typing.TupleMeta) and # type: ignore
not hasattr(annotation, '__tuple_params__')): # for Python 3.6
params = annotation.__args__
if params:
param_str = ', '.join(stringify(p) for p in params)
return '%s[%s]' % (qualname, param_str)
else:
return qualname
elif isinstance(annotation, typing.GenericMeta):
params = None
if hasattr(annotation, '__args__'):
# for Python 3.5.2+
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
params = annotation.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(stringify(arg) for arg
in annotation.__args__[:-1]) # type: ignore
result = stringify(annotation.__args__[-1]) # type: ignore
return '%s[[%s], %s]' % (qualname, args, result)
elif hasattr(annotation, '__parameters__'):
# for Python 3.5.0 and 3.5.1
params = annotation.__parameters__ # type: ignore
if params is not None:
param_str = ', '.join(stringify(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(typing, 'UnionMeta') and
isinstance(annotation, typing.UnionMeta) and # type: ignore
hasattr(annotation, '__union_params__')): # for Python 3.5
params = annotation.__union_params__
if params is not None:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % stringify(params[0])
else:
param_str = ', '.join(stringify(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(annotation, '__origin__') and
annotation.__origin__ is typing.Union): # for Python 3.5.2+
params = annotation.__args__
if params is not None:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % stringify(params[0])
else:
param_str = ', '.join(stringify(p) for p in params)
return 'Union[%s]' % param_str
elif (isinstance(annotation, typing.CallableMeta) and # type: ignore
getattr(annotation, '__args__', None) is not None and
hasattr(annotation, '__result__')): # for Python 3.5
# Skipped in the case of plain typing.Callable
args = annotation.__args__
if args is None:
return qualname
elif args is Ellipsis:
args_str = '...'
else:
formatted_args = (stringify(a) for a in args)
args_str = '[%s]' % ', '.join(formatted_args)
return '%s[%s, %s]' % (qualname,
args_str,
stringify(annotation.__result__))
elif (isinstance(annotation, typing.TupleMeta) and # type: ignore
hasattr(annotation, '__tuple_params__') and
hasattr(annotation, '__tuple_use_ellipsis__')): # for Python 3.5
params = annotation.__tuple_params__
if params is not None:
param_strings = [stringify(p) for p in params]
if annotation.__tuple_use_ellipsis__:
param_strings.append('...')
return '%s[%s]' % (qualname,
', '.join(param_strings))
return qualname

98
tests/test_util_typing.py Normal file
View File

@ -0,0 +1,98 @@
"""
test_util_typing
~~~~~~~~~~~~~~~~
Tests util.typing functions.
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import sys
from numbers import Integral
from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional
from sphinx.util.typing import stringify
class MyClass1:
pass
class MyClass2(MyClass1):
__qualname__ = '<MyClass2>'
def test_stringify():
assert stringify(int) == "int"
assert stringify(str) == "str"
assert stringify(None) == "None"
assert stringify(Integral) == "numbers.Integral"
assert stringify(Any) == "Any"
def test_stringify_type_hints_containers():
assert stringify(List) == "List"
assert stringify(Dict) == "Dict"
assert stringify(List[int]) == "List[int]"
assert stringify(List[str]) == "List[str]"
assert stringify(Dict[str, float]) == "Dict[str, float]"
assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]"
assert stringify(Tuple[str, ...]) == "Tuple[str, ...]"
assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
def test_stringify_type_hints_string():
assert stringify("int") == "int"
assert stringify("str") == "str"
assert stringify(List["int"]) == "List[int]"
assert stringify("Tuple[str]") == "Tuple[str]"
assert stringify("unknown") == "unknown"
def test_stringify_type_hints_Callable():
assert stringify(Callable) == "Callable"
if sys.version_info >= (3, 7):
assert stringify(Callable[[str], int]) == "Callable[[str], int]"
assert stringify(Callable[..., int]) == "Callable[[...], int]"
else:
assert stringify(Callable[[str], int]) == "Callable[str, int]"
assert stringify(Callable[..., int]) == "Callable[..., int]"
def test_stringify_type_hints_Union():
assert stringify(Optional[int]) == "Optional[int]"
assert stringify(Union[str, None]) == "Optional[str]"
assert stringify(Union[int, str]) == "Union[int, str]"
if sys.version_info >= (3, 7):
assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]"
assert (stringify(Union[MyClass1, MyClass2]) ==
"Union[test_util_typing.MyClass1, test_util_typing.<MyClass2>]")
else:
assert stringify(Union[int, Integral]) == "numbers.Integral"
assert stringify(Union[MyClass1, MyClass2]) == "test_util_typing.MyClass1"
def test_stringify_type_hints_typevars():
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
assert stringify(T) == "T"
assert stringify(T_co) == "T_co"
assert stringify(T_contra) == "T_contra"
assert stringify(List[T]) == "List[T]"
def test_stringify_type_hints_custom_class():
assert stringify(MyClass1) == "test_util_typing.MyClass1"
assert stringify(MyClass2) == "test_util_typing.<MyClass2>"
def test_stringify_type_hints_alias():
MyStr = str
MyTuple = Tuple[str, str]
assert stringify(MyStr) == "str"
assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore