refactor: Add sphinx.util.inspect.signature()

As a successor of sphinx.util.inspect.Singnature, this adds
signature() function behaves like `inspect.signature()`.  It is
very similar to way of python's inspect module.

In addition, this also adds stringify_annotation() helper to
sphinx.util.inspect module.  With these two functions, we can move
to python's Signature object to represent function signatures
perfectly.  It's natural design for python developers than ever.
This commit is contained in:
Takeshi KOMIYA 2020-01-08 22:57:24 +09:00
parent ae8fc43024
commit 5867416612
6 changed files with 214 additions and 124 deletions

View File

@ -21,9 +21,7 @@ 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()``
* ``sphinx.util.inspect.Signature``
Features added
--------------

View File

@ -81,20 +81,11 @@ The following is a list of deprecated interfaces.
- 4.0
- N/A
* - ``sphinx.util.inspect.Signature.format_annotation()``
* - ``sphinx.util.inspect.Signature``
- 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.util.inspect.signature`` and
``sphinx.util.inspect.stringify_signature()``
* - ``sphinx.builders.gettext.POHEADER``
- 2.3

View File

@ -33,7 +33,7 @@ from sphinx.util import logging
from sphinx.util import rpartition
from sphinx.util.docstrings import prepare_docstring
from sphinx.util.inspect import (
Signature, getdoc, object_description, safe_getattr, safe_getmembers
getdoc, object_description, safe_getattr, safe_getmembers, stringify_signature
)
if False:
@ -983,9 +983,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
not inspect.isbuiltin(self.object) and
not inspect.isclass(self.object) and
hasattr(self.object, '__call__')):
args = Signature(self.object.__call__).format_args(**kwargs)
sig = inspect.signature(self.object.__call__)
else:
args = Signature(self.object).format_args(**kwargs)
sig = inspect.signature(self.object)
args = stringify_signature(sig, **kwargs)
except TypeError:
if (inspect.is_builtin_class_method(self.object, '__new__') and
inspect.is_builtin_class_method(self.object, '__init__')):
@ -995,11 +996,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
# typing) we try to use the constructor signature as function
# signature without the first argument.
try:
sig = Signature(self.object.__new__, bound_method=True, has_retval=False)
args = sig.format_args(**kwargs)
sig = inspect.signature(self.object.__new__, bound_method=True)
args = stringify_signature(sig, show_return_annotation=False, **kwargs)
except TypeError:
sig = Signature(self.object.__init__, bound_method=True, has_retval=False)
args = sig.format_args(**kwargs)
sig = inspect.signature(self.object.__init__, bound_method=True)
args = stringify_signature(sig, show_return_annotation=False, **kwargs)
# escape backslashes for reST
args = args.replace('\\', '\\\\')
@ -1080,8 +1081,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)):
return None
try:
sig = Signature(initmeth, bound_method=True, has_retval=False)
return sig.format_args(**kwargs)
sig = inspect.signature(initmeth, bound_method=True)
return stringify_signature(sig, show_return_annotation=False, **kwargs)
except TypeError:
# still not possible: happens e.g. for old-style classes
# with __init__ in C
@ -1283,9 +1284,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
# can never get arguments of a C function or method
return None
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
args = Signature(self.object, bound_method=False).format_args(**kwargs)
sig = inspect.signature(self.object, bound_method=False)
else:
args = Signature(self.object, bound_method=True).format_args(**kwargs)
sig = inspect.signature(self.object, bound_method=True)
args = stringify_signature(sig, **kwargs)
# escape backslashes for reST
args = args.replace('\\', '\\\\')
return args

View File

@ -315,6 +315,112 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls
def signature(subject: Callable, bound_method: bool = False) -> inspect.Signature:
"""Return a Signature object for the given *subject*.
:param bound_method: Specify *subject* is a bound method or not
"""
# check subject is not a built-in class (ex. int, str)
if (isinstance(subject, type) and
is_builtin_class_method(subject, "__new__") and
is_builtin_class_method(subject, "__init__")):
raise TypeError("can't compute signature for built-in type {}".format(subject))
try:
signature = inspect.signature(subject)
parameters = list(signature.parameters.values())
return_annotation = signature.return_annotation
except IndexError:
# Until python 3.6.4, cpython has been crashed on inspection for
# partialmethods not having any arguments.
# https://bugs.python.org/issue33009
if hasattr(subject, '_partialmethod'):
parameters = []
return_annotation = inspect.Parameter.empty
else:
raise
try:
# Update unresolved annotations using ``get_type_hints()``.
annotations = typing.get_type_hints(subject)
for i, param in enumerate(parameters):
if isinstance(param.annotation, str) and param.name in annotations:
parameters[i] = param.replace(annotation=annotations[param.name])
if 'return' in annotations:
return_annotation = annotations['return']
except Exception:
# ``get_type_hints()`` does not support some kind of objects like partial,
# ForwardRef and so on.
pass
if bound_method:
if inspect.ismethod(subject):
# ``inspect.signature()`` considers the subject is a bound method and removes
# first argument from signature. Therefore no skips are needed here.
pass
else:
if len(parameters) > 0:
parameters.pop(0)
return inspect.Signature(parameters, return_annotation=return_annotation)
def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
show_return_annotation: bool = True) -> str:
"""Stringify a Signature object.
:param show_annotation: Show annotation in result
"""
args = []
last_kind = None
for param in sig.parameters.values():
# insert '*' between POSITIONAL args and KEYWORD_ONLY args::
# func(a, b, *, c, d):
if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
param.POSITIONAL_ONLY,
None):
args.append('*')
arg = StringIO()
if param.kind in (param.POSITIONAL_ONLY,
param.POSITIONAL_OR_KEYWORD,
param.KEYWORD_ONLY):
arg.write(param.name)
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(stringify_annotation(param.annotation))
if param.default is not param.empty:
if show_annotation and param.annotation is not param.empty:
arg.write(' = ')
arg.write(object_description(param.default))
else:
arg.write('=')
arg.write(object_description(param.default))
elif param.kind == param.VAR_POSITIONAL:
arg.write('*')
arg.write(param.name)
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(stringify_annotation(param.annotation))
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(stringify_annotation(param.annotation))
args.append(arg.getvalue())
last_kind = param.kind
if (sig.return_annotation is inspect.Parameter.empty or
show_annotation is False or
show_return_annotation is False):
return '(%s)' % ', '.join(args)
else:
annotation = stringify_annotation(sig.return_annotation)
return '(%s) -> %s' % (', '.join(args), annotation)
class Parameter:
"""Fake parameter class for python2."""
POSITIONAL_ONLY = 0
@ -342,6 +448,9 @@ class Signature:
def __init__(self, subject: Callable, bound_method: bool = False,
has_retval: bool = True) -> None:
warnings.warn('sphinx.util.inspect.Signature() is deprecated',
RemovedInSphinx40Warning)
# check subject is not a built-in class (ex. int, str)
if (isinstance(subject, type) and
is_builtin_class_method(subject, "__new__") and
@ -467,20 +576,14 @@ class Signature:
def format_annotation(self, annotation: Any) -> str:
"""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+"""
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"""
warnings.warn('format_annotation_old() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation)

View File

@ -1318,13 +1318,13 @@ def test_partialmethod(app):
' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod',
' ',
' ',
' .. py:method:: Cell.set_alive() -> None',
' .. py:method:: Cell.set_alive()',
' :module: target.partialmethod',
' ',
' Make a cell alive.',
' ',
' ',
' .. py:method:: Cell.set_dead() -> None',
' .. py:method:: Cell.set_dead()',
' :module: target.partialmethod',
' ',
' Make a cell dead.',
@ -1336,11 +1336,6 @@ def test_partialmethod(app):
' Update state of cell to *state*.',
' ',
]
if (sys.version_info < (3, 5, 4) or
(3, 6, 5) <= sys.version_info < (3, 7) or
(3, 7, 0, 'beta', 3) <= sys.version_info):
# TODO: this condition should be updated after 3.7-final release.
expected = '\n'.join(expected).replace(' -> None', '').split('\n')
options = {"members": None}
actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options)

View File

@ -17,6 +17,7 @@ import types
import pytest
from sphinx.util import inspect
from sphinx.util.inspect import stringify_signature
def test_getargspec():
@ -89,39 +90,39 @@ def test_getargspec_bound_methods():
assert expected_bound == inspect.getargspec(wrapped_bound_method)
def test_Signature():
def test_signature():
# literals
with pytest.raises(TypeError):
inspect.Signature(1)
inspect.signature(1)
with pytest.raises(TypeError):
inspect.Signature('')
inspect.signature('')
# builitin classes
with pytest.raises(TypeError):
inspect.Signature(int)
inspect.signature(int)
with pytest.raises(TypeError):
inspect.Signature(str)
inspect.signature(str)
# normal function
def func(a, b, c=1, d=2, *e, **f):
pass
sig = inspect.Signature(func).format_args()
sig = inspect.stringify_signature(inspect.signature(func))
assert sig == '(a, b, c=1, d=2, *e, **f)'
def test_Signature_partial():
def test_signature_partial():
def fun(a, b, c=1, d=2):
pass
p = functools.partial(fun, 10, c=11)
sig = inspect.Signature(p).format_args()
assert sig == '(b, *, c=11, d=2)'
sig = inspect.signature(p)
assert stringify_signature(sig) == '(b, *, c=11, d=2)'
def test_Signature_methods():
def test_signature_methods():
class Foo:
def meth1(self, arg1, **kwargs):
pass
@ -139,36 +140,36 @@ def test_Signature_methods():
pass
# unbound method
sig = inspect.Signature(Foo.meth1).format_args()
assert sig == '(self, arg1, **kwargs)'
sig = inspect.signature(Foo.meth1)
assert stringify_signature(sig) == '(self, arg1, **kwargs)'
sig = inspect.Signature(Foo.meth1, bound_method=True).format_args()
assert sig == '(arg1, **kwargs)'
sig = inspect.signature(Foo.meth1, bound_method=True)
assert stringify_signature(sig) == '(arg1, **kwargs)'
# bound method
sig = inspect.Signature(Foo().meth1).format_args()
assert sig == '(arg1, **kwargs)'
sig = inspect.signature(Foo().meth1)
assert stringify_signature(sig) == '(arg1, **kwargs)'
# class method
sig = inspect.Signature(Foo.meth2).format_args()
assert sig == '(arg1, *args, **kwargs)'
sig = inspect.signature(Foo.meth2)
assert stringify_signature(sig) == '(arg1, *args, **kwargs)'
sig = inspect.Signature(Foo().meth2).format_args()
assert sig == '(arg1, *args, **kwargs)'
sig = inspect.signature(Foo().meth2)
assert stringify_signature(sig) == '(arg1, *args, **kwargs)'
# static method
sig = inspect.Signature(Foo.meth3).format_args()
assert sig == '(arg1, *args, **kwargs)'
sig = inspect.signature(Foo.meth3)
assert stringify_signature(sig) == '(arg1, *args, **kwargs)'
sig = inspect.Signature(Foo().meth3).format_args()
assert sig == '(arg1, *args, **kwargs)'
sig = inspect.signature(Foo().meth3)
assert stringify_signature(sig) == '(arg1, *args, **kwargs)'
# wrapped bound method
sig = inspect.Signature(wrapped_bound_method).format_args()
assert sig == '(arg1, **kwargs)'
sig = inspect.signature(wrapped_bound_method)
assert stringify_signature(sig) == '(arg1, **kwargs)'
def test_Signature_partialmethod():
def test_signature_partialmethod():
from functools import partialmethod
class Foo:
@ -183,116 +184,115 @@ def test_Signature_partialmethod():
baz = partialmethod(meth2, 1, 2)
subject = Foo()
sig = inspect.Signature(subject.foo).format_args()
assert sig == '(arg3=None, arg4=None)'
sig = inspect.signature(subject.foo)
assert stringify_signature(sig) == '(arg3=None, arg4=None)'
sig = inspect.Signature(subject.bar).format_args()
assert sig == '(arg2, *, arg3=3, arg4=None)'
sig = inspect.signature(subject.bar)
assert stringify_signature(sig) == '(arg2, *, arg3=3, arg4=None)'
sig = inspect.Signature(subject.baz).format_args()
assert sig == '()'
sig = inspect.signature(subject.baz)
assert stringify_signature(sig) == '()'
def test_Signature_annotations():
def test_signature_annotations():
from typing_test_data import (f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10,
f11, f12, f13, f14, f15, f16, f17, f18, f19, Node)
# Class annotations
sig = inspect.Signature(f0).format_args()
assert sig == '(x: int, y: numbers.Integral) -> None'
sig = inspect.signature(f0)
assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None'
# Generic types with concrete parameters
sig = inspect.Signature(f1).format_args()
assert sig == '(x: List[int]) -> List[int]'
sig = inspect.signature(f1)
assert stringify_signature(sig) == '(x: List[int]) -> List[int]'
# TypeVars and generic types with TypeVars
sig = inspect.Signature(f2).format_args()
assert sig == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]'
sig = inspect.signature(f2)
assert stringify_signature(sig) == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]'
# Union types
sig = inspect.Signature(f3).format_args()
assert sig == '(x: Union[str, numbers.Integral]) -> None'
sig = inspect.signature(f3)
assert stringify_signature(sig) == '(x: Union[str, numbers.Integral]) -> None'
# Quoted annotations
sig = inspect.Signature(f4).format_args()
assert sig == '(x: str, y: str) -> None'
sig = inspect.signature(f4)
assert stringify_signature(sig) == '(x: str, y: str) -> None'
# Keyword-only arguments
sig = inspect.Signature(f5).format_args()
assert sig == '(x: int, *, y: str, z: str) -> None'
sig = inspect.signature(f5)
assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None'
# Keyword-only arguments with varargs
sig = inspect.Signature(f6).format_args()
assert sig == '(x: int, *args, y: str, z: str) -> None'
sig = inspect.signature(f6)
assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None'
# Space around '=' for defaults
sig = inspect.Signature(f7).format_args()
assert sig == '(x: int = None, y: dict = {}) -> None'
sig = inspect.signature(f7)
assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None'
# Callable types
sig = inspect.Signature(f8).format_args()
assert sig == '(x: Callable[[int, str], int]) -> None'
sig = inspect.signature(f8)
assert stringify_signature(sig) == '(x: Callable[[int, str], int]) -> None'
sig = inspect.Signature(f9).format_args()
assert sig == '(x: Callable) -> None'
sig = inspect.signature(f9)
assert stringify_signature(sig) == '(x: Callable) -> None'
# Tuple types
sig = inspect.Signature(f10).format_args()
assert sig == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None'
sig = inspect.signature(f10)
assert stringify_signature(sig) == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None'
# Instance annotations
sig = inspect.Signature(f11).format_args()
assert sig == '(x: CustomAnnotation, y: 123) -> None'
# has_retval=False
sig = inspect.Signature(f11, has_retval=False).format_args()
assert sig == '(x: CustomAnnotation, y: 123)'
sig = inspect.signature(f11)
assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None'
# tuple with more than two items
sig = inspect.Signature(f12).format_args()
assert sig == '() -> Tuple[int, str, int]'
sig = inspect.signature(f12)
assert stringify_signature(sig) == '() -> Tuple[int, str, int]'
# optional
sig = inspect.Signature(f13).format_args()
assert sig == '() -> Optional[str]'
sig = inspect.signature(f13)
assert stringify_signature(sig) == '() -> Optional[str]'
# Any
sig = inspect.Signature(f14).format_args()
assert sig == '() -> Any'
sig = inspect.signature(f14)
assert stringify_signature(sig) == '() -> Any'
# ForwardRef
sig = inspect.Signature(f15).format_args()
assert sig == '(x: Unknown, y: int) -> Any'
sig = inspect.signature(f15)
assert stringify_signature(sig) == '(x: Unknown, y: int) -> Any'
# keyword only arguments (1)
sig = inspect.Signature(f16).format_args()
assert sig == '(arg1, arg2, *, arg3=None, arg4=None)'
sig = inspect.signature(f16)
assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)'
# keyword only arguments (2)
sig = inspect.Signature(f17).format_args()
assert sig == '(*, arg3, arg4)'
sig = inspect.signature(f17)
assert stringify_signature(sig) == '(*, arg3, arg4)'
sig = inspect.Signature(f18).format_args()
assert sig == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]'
sig = inspect.signature(f18)
assert stringify_signature(sig) == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]'
# annotations for variadic and keyword parameters
sig = inspect.Signature(f19).format_args()
assert sig == '(*args: int, **kwargs: str)'
sig = inspect.signature(f19)
assert stringify_signature(sig) == '(*args: int, **kwargs: str)'
# type hints by string
sig = inspect.Signature(Node.children).format_args()
sig = inspect.signature(Node.children)
if (3, 5, 0) <= sys.version_info < (3, 5, 3):
assert sig == '(self) -> List[Node]'
assert stringify_signature(sig) == '(self) -> List[Node]'
else:
assert sig == '(self) -> List[typing_test_data.Node]'
assert stringify_signature(sig) == '(self) -> List[typing_test_data.Node]'
sig = inspect.Signature(Node.__init__).format_args()
assert sig == '(self, parent: Optional[Node]) -> None'
sig = inspect.signature(Node.__init__)
assert stringify_signature(sig) == '(self, parent: Optional[Node]) -> None'
# show_annotation is False
sig = inspect.Signature(f7).format_args(show_annotation=False)
assert sig == '(x=None, y={})'
sig = inspect.signature(f7)
assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})'
# show_return_annotation is False
sig = inspect.signature(f7)
assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})'
def test_safe_getattr_with_default():
class Foo: