mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
gh-112139: Add inspect.Signature.format
and use it in pydoc
(#112143)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
parent
0229d2a9b1
commit
a9574c68f0
6 changed files with 205 additions and 11 deletions
|
@ -753,6 +753,17 @@ function.
|
||||||
Signature objects are also supported by generic function
|
Signature objects are also supported by generic function
|
||||||
:func:`copy.replace`.
|
:func:`copy.replace`.
|
||||||
|
|
||||||
|
.. method:: format(*, max_width=None)
|
||||||
|
|
||||||
|
Convert signature object to string.
|
||||||
|
|
||||||
|
If *max_width* is passed, the method will attempt to fit
|
||||||
|
the signature into lines of at most *max_width* characters.
|
||||||
|
If the signature is longer than *max_width*,
|
||||||
|
all parameters will be on separate lines.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
|
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
|
||||||
|
|
||||||
Return a :class:`Signature` (or its subclass) object for a given callable
|
Return a :class:`Signature` (or its subclass) object for a given callable
|
||||||
|
|
|
@ -3316,6 +3316,16 @@ class Signature:
|
||||||
return '<{} {}>'.format(self.__class__.__name__, self)
|
return '<{} {}>'.format(self.__class__.__name__, self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
return self.format()
|
||||||
|
|
||||||
|
def format(self, *, max_width=None):
|
||||||
|
"""Convert signature object to string.
|
||||||
|
|
||||||
|
If *max_width* integer is passed,
|
||||||
|
signature will try to fit into the *max_width*.
|
||||||
|
If signature is longer than *max_width*,
|
||||||
|
all parameters will be on separate lines.
|
||||||
|
"""
|
||||||
result = []
|
result = []
|
||||||
render_pos_only_separator = False
|
render_pos_only_separator = False
|
||||||
render_kw_only_separator = True
|
render_kw_only_separator = True
|
||||||
|
@ -3353,6 +3363,8 @@ class Signature:
|
||||||
result.append('/')
|
result.append('/')
|
||||||
|
|
||||||
rendered = '({})'.format(', '.join(result))
|
rendered = '({})'.format(', '.join(result))
|
||||||
|
if max_width is not None and len(rendered) > max_width:
|
||||||
|
rendered = '(\n {}\n)'.format(',\n '.join(result))
|
||||||
|
|
||||||
if self.return_annotation is not _empty:
|
if self.return_annotation is not _empty:
|
||||||
anno = formatannotation(self.return_annotation)
|
anno = formatannotation(self.return_annotation)
|
||||||
|
|
|
@ -201,7 +201,10 @@ def _getargspec(object):
|
||||||
try:
|
try:
|
||||||
signature = inspect.signature(object)
|
signature = inspect.signature(object)
|
||||||
if signature:
|
if signature:
|
||||||
return str(signature)
|
name = getattr(object, '__name__', '')
|
||||||
|
# <lambda> function are always single-line and should not be formatted
|
||||||
|
max_width = (80 - len(name)) if name != '<lambda>' else None
|
||||||
|
return signature.format(max_width=max_width)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
argspec = getattr(object, '__text_signature__', None)
|
argspec = getattr(object, '__text_signature__', None)
|
||||||
if argspec:
|
if argspec:
|
||||||
|
|
|
@ -3796,26 +3796,36 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
self.assertEqual(str(inspect.signature(foo)),
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
'(a: int = 1, *, b, c=None, **kwargs) -> 42')
|
'(a: int = 1, *, b, c=None, **kwargs) -> 42')
|
||||||
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
|
inspect.signature(foo).format())
|
||||||
|
|
||||||
def foo(a:int=1, *args, b, c=None, **kwargs) -> 42:
|
def foo(a:int=1, *args, b, c=None, **kwargs) -> 42:
|
||||||
pass
|
pass
|
||||||
self.assertEqual(str(inspect.signature(foo)),
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
'(a: int = 1, *args, b, c=None, **kwargs) -> 42')
|
'(a: int = 1, *args, b, c=None, **kwargs) -> 42')
|
||||||
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
|
inspect.signature(foo).format())
|
||||||
|
|
||||||
def foo():
|
def foo():
|
||||||
pass
|
pass
|
||||||
self.assertEqual(str(inspect.signature(foo)), '()')
|
self.assertEqual(str(inspect.signature(foo)), '()')
|
||||||
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
|
inspect.signature(foo).format())
|
||||||
|
|
||||||
def foo(a: list[str]) -> tuple[str, float]:
|
def foo(a: list[str]) -> tuple[str, float]:
|
||||||
pass
|
pass
|
||||||
self.assertEqual(str(inspect.signature(foo)),
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
'(a: list[str]) -> tuple[str, float]')
|
'(a: list[str]) -> tuple[str, float]')
|
||||||
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
|
inspect.signature(foo).format())
|
||||||
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
def foo(a: list[str]) -> Tuple[str, float]:
|
def foo(a: list[str]) -> Tuple[str, float]:
|
||||||
pass
|
pass
|
||||||
self.assertEqual(str(inspect.signature(foo)),
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
'(a: list[str]) -> Tuple[str, float]')
|
'(a: list[str]) -> Tuple[str, float]')
|
||||||
|
self.assertEqual(str(inspect.signature(foo)),
|
||||||
|
inspect.signature(foo).format())
|
||||||
|
|
||||||
def test_signature_str_positional_only(self):
|
def test_signature_str_positional_only(self):
|
||||||
P = inspect.Parameter
|
P = inspect.Parameter
|
||||||
|
@ -3826,19 +3836,85 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(str(inspect.signature(test)),
|
self.assertEqual(str(inspect.signature(test)),
|
||||||
'(a_po, /, *, b, **kwargs)')
|
'(a_po, /, *, b, **kwargs)')
|
||||||
|
self.assertEqual(str(inspect.signature(test)),
|
||||||
|
inspect.signature(test).format())
|
||||||
|
|
||||||
self.assertEqual(str(S(parameters=[P('foo', P.POSITIONAL_ONLY)])),
|
test = S(parameters=[P('foo', P.POSITIONAL_ONLY)])
|
||||||
'(foo, /)')
|
self.assertEqual(str(test), '(foo, /)')
|
||||||
|
self.assertEqual(str(test), test.format())
|
||||||
|
|
||||||
self.assertEqual(str(S(parameters=[
|
test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
|
||||||
P('foo', P.POSITIONAL_ONLY),
|
P('bar', P.VAR_KEYWORD)])
|
||||||
P('bar', P.VAR_KEYWORD)])),
|
self.assertEqual(str(test), '(foo, /, **bar)')
|
||||||
'(foo, /, **bar)')
|
self.assertEqual(str(test), test.format())
|
||||||
|
|
||||||
self.assertEqual(str(S(parameters=[
|
test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
|
||||||
P('foo', P.POSITIONAL_ONLY),
|
P('bar', P.VAR_POSITIONAL)])
|
||||||
P('bar', P.VAR_POSITIONAL)])),
|
self.assertEqual(str(test), '(foo, /, *bar)')
|
||||||
'(foo, /, *bar)')
|
self.assertEqual(str(test), test.format())
|
||||||
|
|
||||||
|
def test_signature_format(self):
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
def func(x: Annotated[int, 'meta'], y: Literal['a', 'b'], z: 'LiteralString'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
expected_singleline = "(x: Annotated[int, 'meta'], y: Literal['a', 'b'], z: 'LiteralString')"
|
||||||
|
expected_multiline = """(
|
||||||
|
x: Annotated[int, 'meta'],
|
||||||
|
y: Literal['a', 'b'],
|
||||||
|
z: 'LiteralString'
|
||||||
|
)"""
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(),
|
||||||
|
expected_singleline,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=None),
|
||||||
|
expected_singleline,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=len(expected_singleline)),
|
||||||
|
expected_singleline,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=len(expected_singleline) - 1),
|
||||||
|
expected_multiline,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=0),
|
||||||
|
expected_multiline,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=-1),
|
||||||
|
expected_multiline,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_signature_format_all_arg_types(self):
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
def func(
|
||||||
|
x: Annotated[int, 'meta'],
|
||||||
|
/,
|
||||||
|
y: Literal['a', 'b'],
|
||||||
|
*,
|
||||||
|
z: 'LiteralString',
|
||||||
|
**kwargs: object,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
expected_multiline = """(
|
||||||
|
x: Annotated[int, 'meta'],
|
||||||
|
/,
|
||||||
|
y: Literal['a', 'b'],
|
||||||
|
*,
|
||||||
|
z: 'LiteralString',
|
||||||
|
**kwargs: object
|
||||||
|
) -> None"""
|
||||||
|
self.assertEqual(
|
||||||
|
inspect.signature(func).format(max_width=-1),
|
||||||
|
expected_multiline,
|
||||||
|
)
|
||||||
|
|
||||||
def test_signature_replace_parameters(self):
|
def test_signature_replace_parameters(self):
|
||||||
def test(a, b) -> 42:
|
def test(a, b) -> 42:
|
||||||
|
|
|
@ -870,6 +870,95 @@ class B(A)
|
||||||
for expected_line in expected_lines:
|
for expected_line in expected_lines:
|
||||||
self.assertIn(expected_line, as_text)
|
self.assertIn(expected_line, as_text)
|
||||||
|
|
||||||
|
def test_long_signatures(self):
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Literal, Annotated
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self,
|
||||||
|
arg1: Callable[[int, int, int], str],
|
||||||
|
arg2: Literal['some value', 'other value'],
|
||||||
|
arg3: Annotated[int, 'some docs about this type'],
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
doc = pydoc.render_doc(A)
|
||||||
|
# clean up the extra text formatting that pydoc performs
|
||||||
|
doc = re.sub('\b.', '', doc)
|
||||||
|
self.assertEqual(doc, '''Python Library Documentation: class A in module %s
|
||||||
|
|
||||||
|
class A(builtins.object)
|
||||||
|
| A(
|
||||||
|
| arg1: collections.abc.Callable[[int, int, int], str],
|
||||||
|
| arg2: Literal['some value', 'other value'],
|
||||||
|
| arg3: Annotated[int, 'some docs about this type']
|
||||||
|
| ) -> None
|
||||||
|
|
|
||||||
|
| Methods defined here:
|
||||||
|
|
|
||||||
|
| __init__(
|
||||||
|
| self,
|
||||||
|
| arg1: collections.abc.Callable[[int, int, int], str],
|
||||||
|
| arg2: Literal['some value', 'other value'],
|
||||||
|
| arg3: Annotated[int, 'some docs about this type']
|
||||||
|
| ) -> None
|
||||||
|
|
|
||||||
|
| ----------------------------------------------------------------------
|
||||||
|
| Data descriptors defined here:
|
||||||
|
|
|
||||||
|
| __dict__
|
||||||
|
| dictionary for instance variables
|
||||||
|
|
|
||||||
|
| __weakref__
|
||||||
|
| list of weak references to the object
|
||||||
|
''' % __name__)
|
||||||
|
|
||||||
|
def func(
|
||||||
|
arg1: Callable[[Annotated[int, 'Some doc']], str],
|
||||||
|
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
) -> Annotated[int, 'Some other']:
|
||||||
|
...
|
||||||
|
|
||||||
|
doc = pydoc.render_doc(func)
|
||||||
|
# clean up the extra text formatting that pydoc performs
|
||||||
|
doc = re.sub('\b.', '', doc)
|
||||||
|
self.assertEqual(doc, '''Python Library Documentation: function func in module %s
|
||||||
|
|
||||||
|
func(
|
||||||
|
arg1: collections.abc.Callable[[typing.Annotated[int, 'Some doc']], str],
|
||||||
|
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
) -> Annotated[int, 'Some other']
|
||||||
|
''' % __name__)
|
||||||
|
|
||||||
|
def function_with_really_long_name_so_annotations_can_be_rather_small(
|
||||||
|
arg1: int,
|
||||||
|
arg2: str,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
|
||||||
|
doc = pydoc.render_doc(function_with_really_long_name_so_annotations_can_be_rather_small)
|
||||||
|
# clean up the extra text formatting that pydoc performs
|
||||||
|
doc = re.sub('\b.', '', doc)
|
||||||
|
self.assertEqual(doc, '''Python Library Documentation: function function_with_really_long_name_so_annotations_can_be_rather_small in module %s
|
||||||
|
|
||||||
|
function_with_really_long_name_so_annotations_can_be_rather_small(
|
||||||
|
arg1: int,
|
||||||
|
arg2: str
|
||||||
|
)
|
||||||
|
''' % __name__)
|
||||||
|
|
||||||
|
does_not_have_name = lambda \
|
||||||
|
very_long_parameter_name_that_should_not_fit_into_a_single_line, \
|
||||||
|
second_very_long_parameter_name: ...
|
||||||
|
|
||||||
|
doc = pydoc.render_doc(does_not_have_name)
|
||||||
|
# clean up the extra text formatting that pydoc performs
|
||||||
|
doc = re.sub('\b.', '', doc)
|
||||||
|
self.assertEqual(doc, '''Python Library Documentation: function <lambda> in module %s
|
||||||
|
|
||||||
|
<lambda> lambda very_long_parameter_name_that_should_not_fit_into_a_single_line, second_very_long_parameter_name
|
||||||
|
''' % __name__)
|
||||||
|
|
||||||
def test__future__imports(self):
|
def test__future__imports(self):
|
||||||
# __future__ features are excluded from module help,
|
# __future__ features are excluded from module help,
|
||||||
# except when it's the __future__ module itself
|
# except when it's the __future__ module itself
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Add :meth:`Signature.format` to format signatures to string with extra options.
|
||||||
|
And use it in :mod:`pydoc` to render more readable signatures that have new
|
||||||
|
lines between parameters.
|
Loading…
Add table
Add a link
Reference in a new issue