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.roles.Index``
* ``sphinx.util.detect_encoding()`` * ``sphinx.util.detect_encoding()``
* ``sphinx.util.get_module_source()`` * ``sphinx.util.get_module_source()``
* ``sphinx.util.inspect.Signature.format_annotation()`` * ``sphinx.util.inspect.Signature``
* ``sphinx.util.inspect.Signature.format_annotation_new()``
* ``sphinx.util.inspect.Signature.format_annotation_old()``
Features added Features added
-------------- --------------

View File

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

View File

@ -33,7 +33,7 @@ from sphinx.util import logging
from sphinx.util import rpartition from sphinx.util import rpartition
from sphinx.util.docstrings import prepare_docstring from sphinx.util.docstrings import prepare_docstring
from sphinx.util.inspect import ( from sphinx.util.inspect import (
Signature, getdoc, object_description, safe_getattr, safe_getmembers getdoc, object_description, safe_getattr, safe_getmembers, stringify_signature
) )
if False: if False:
@ -983,9 +983,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
not inspect.isbuiltin(self.object) and not inspect.isbuiltin(self.object) and
not inspect.isclass(self.object) and not inspect.isclass(self.object) and
hasattr(self.object, '__call__')): hasattr(self.object, '__call__')):
args = Signature(self.object.__call__).format_args(**kwargs) sig = inspect.signature(self.object.__call__)
else: else:
args = Signature(self.object).format_args(**kwargs) sig = inspect.signature(self.object)
args = stringify_signature(sig, **kwargs)
except TypeError: except TypeError:
if (inspect.is_builtin_class_method(self.object, '__new__') and if (inspect.is_builtin_class_method(self.object, '__new__') and
inspect.is_builtin_class_method(self.object, '__init__')): 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 # typing) we try to use the constructor signature as function
# signature without the first argument. # signature without the first argument.
try: try:
sig = Signature(self.object.__new__, bound_method=True, has_retval=False) sig = inspect.signature(self.object.__new__, bound_method=True)
args = sig.format_args(**kwargs) args = stringify_signature(sig, show_return_annotation=False, **kwargs)
except TypeError: except TypeError:
sig = Signature(self.object.__init__, bound_method=True, has_retval=False) sig = inspect.signature(self.object.__init__, bound_method=True)
args = sig.format_args(**kwargs) args = stringify_signature(sig, show_return_annotation=False, **kwargs)
# escape backslashes for reST # escape backslashes for reST
args = args.replace('\\', '\\\\') args = args.replace('\\', '\\\\')
@ -1080,8 +1081,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)):
return None return None
try: try:
sig = Signature(initmeth, bound_method=True, has_retval=False) sig = inspect.signature(initmeth, bound_method=True)
return sig.format_args(**kwargs) return stringify_signature(sig, show_return_annotation=False, **kwargs)
except TypeError: except TypeError:
# still not possible: happens e.g. for old-style classes # still not possible: happens e.g. for old-style classes
# with __init__ in C # with __init__ in C
@ -1283,9 +1284,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
# can never get arguments of a C function or method # can never get arguments of a C function or method
return None return None
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): 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: 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 # escape backslashes for reST
args = args.replace('\\', '\\\\') args = args.replace('\\', '\\\\')
return args 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 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: class Parameter:
"""Fake parameter class for python2.""" """Fake parameter class for python2."""
POSITIONAL_ONLY = 0 POSITIONAL_ONLY = 0
@ -342,6 +448,9 @@ class Signature:
def __init__(self, subject: Callable, bound_method: bool = False, def __init__(self, subject: Callable, bound_method: bool = False,
has_retval: bool = True) -> None: 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) # check subject is not a built-in class (ex. int, str)
if (isinstance(subject, type) and if (isinstance(subject, type) and
is_builtin_class_method(subject, "__new__") and is_builtin_class_method(subject, "__new__") and
@ -467,20 +576,14 @@ class Signature:
def format_annotation(self, annotation: Any) -> str: def format_annotation(self, annotation: Any) -> str:
"""Return formatted representation of a type annotation.""" """Return formatted representation of a type annotation."""
warnings.warn('format_annotation() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation) return stringify_annotation(annotation)
def format_annotation_new(self, annotation: Any) -> str: def format_annotation_new(self, annotation: Any) -> str:
"""format_annotation() for py37+""" """format_annotation() for py37+"""
warnings.warn('format_annotation_new() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation) return stringify_annotation(annotation)
def format_annotation_old(self, annotation: Any) -> str: def format_annotation_old(self, annotation: Any) -> str:
"""format_annotation() for py36 or below""" """format_annotation() for py36 or below"""
warnings.warn('format_annotation_old() is deprecated',
RemovedInSphinx40Warning)
return stringify_annotation(annotation) 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', ' 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', ' :module: target.partialmethod',
' ', ' ',
' Make a cell alive.', ' Make a cell alive.',
' ', ' ',
' ', ' ',
' .. py:method:: Cell.set_dead() -> None', ' .. py:method:: Cell.set_dead()',
' :module: target.partialmethod', ' :module: target.partialmethod',
' ', ' ',
' Make a cell dead.', ' Make a cell dead.',
@ -1336,11 +1336,6 @@ def test_partialmethod(app):
' Update state of cell to *state*.', ' 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} options = {"members": None}
actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options)

View File

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