Merge pull request #7002 from tk0miya/refactor_Signature2

refactor: Add sphinx.util.inspect.signature()
This commit is contained in:
Takeshi KOMIYA 2020-01-11 01:24:09 +09:00 committed by GitHub
commit 6fa592f111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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: