Skip to content

Commit a9574c6

Browse files
gh-112139: Add inspect.Signature.format and use it in pydoc (#112143)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 0229d2a commit a9574c6

File tree

6 files changed

+205
-11
lines changed

6 files changed

+205
-11
lines changed

Doc/library/inspect.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,17 @@ function.
753753
Signature objects are also supported by generic function
754754
:func:`copy.replace`.
755755

756+
.. method:: format(*, max_width=None)
757+
758+
Convert signature object to string.
759+
760+
If *max_width* is passed, the method will attempt to fit
761+
the signature into lines of at most *max_width* characters.
762+
If the signature is longer than *max_width*,
763+
all parameters will be on separate lines.
764+
765+
.. versionadded:: 3.13
766+
756767
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
757768

758769
Return a :class:`Signature` (or its subclass) object for a given callable

Lib/inspect.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3316,6 +3316,16 @@ def __repr__(self):
33163316
return '<{} {}>'.format(self.__class__.__name__, self)
33173317

33183318
def __str__(self):
3319+
return self.format()
3320+
3321+
def format(self, *, max_width=None):
3322+
"""Convert signature object to string.
3323+
3324+
If *max_width* integer is passed,
3325+
signature will try to fit into the *max_width*.
3326+
If signature is longer than *max_width*,
3327+
all parameters will be on separate lines.
3328+
"""
33193329
result = []
33203330
render_pos_only_separator = False
33213331
render_kw_only_separator = True
@@ -3353,6 +3363,8 @@ def __str__(self):
33533363
result.append('/')
33543364

33553365
rendered = '({})'.format(', '.join(result))
3366+
if max_width is not None and len(rendered) > max_width:
3367+
rendered = '(\n {}\n)'.format(',\n '.join(result))
33563368

33573369
if self.return_annotation is not _empty:
33583370
anno = formatannotation(self.return_annotation)

Lib/pydoc.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ def _getargspec(object):
201201
try:
202202
signature = inspect.signature(object)
203203
if signature:
204-
return str(signature)
204+
name = getattr(object, '__name__', '')
205+
# <lambda> function are always single-line and should not be formatted
206+
max_width = (80 - len(name)) if name != '<lambda>' else None
207+
return signature.format(max_width=max_width)
205208
except (ValueError, TypeError):
206209
argspec = getattr(object, '__text_signature__', None)
207210
if argspec:

Lib/test/test_inspect/test_inspect.py

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3796,26 +3796,36 @@ def foo(a:int=1, *, b, c=None, **kwargs) -> 42:
37963796
pass
37973797
self.assertEqual(str(inspect.signature(foo)),
37983798
'(a: int = 1, *, b, c=None, **kwargs) -> 42')
3799+
self.assertEqual(str(inspect.signature(foo)),
3800+
inspect.signature(foo).format())
37993801

38003802
def foo(a:int=1, *args, b, c=None, **kwargs) -> 42:
38013803
pass
38023804
self.assertEqual(str(inspect.signature(foo)),
38033805
'(a: int = 1, *args, b, c=None, **kwargs) -> 42')
3806+
self.assertEqual(str(inspect.signature(foo)),
3807+
inspect.signature(foo).format())
38043808

38053809
def foo():
38063810
pass
38073811
self.assertEqual(str(inspect.signature(foo)), '()')
3812+
self.assertEqual(str(inspect.signature(foo)),
3813+
inspect.signature(foo).format())
38083814

38093815
def foo(a: list[str]) -> tuple[str, float]:
38103816
pass
38113817
self.assertEqual(str(inspect.signature(foo)),
38123818
'(a: list[str]) -> tuple[str, float]')
3819+
self.assertEqual(str(inspect.signature(foo)),
3820+
inspect.signature(foo).format())
38133821

38143822
from typing import Tuple
38153823
def foo(a: list[str]) -> Tuple[str, float]:
38163824
pass
38173825
self.assertEqual(str(inspect.signature(foo)),
38183826
'(a: list[str]) -> Tuple[str, float]')
3827+
self.assertEqual(str(inspect.signature(foo)),
3828+
inspect.signature(foo).format())
38193829

38203830
def test_signature_str_positional_only(self):
38213831
P = inspect.Parameter
@@ -3826,19 +3836,85 @@ def test(a_po, /, *, b, **kwargs):
38263836

38273837
self.assertEqual(str(inspect.signature(test)),
38283838
'(a_po, /, *, b, **kwargs)')
3839+
self.assertEqual(str(inspect.signature(test)),
3840+
inspect.signature(test).format())
3841+
3842+
test = S(parameters=[P('foo', P.POSITIONAL_ONLY)])
3843+
self.assertEqual(str(test), '(foo, /)')
3844+
self.assertEqual(str(test), test.format())
38293845

3830-
self.assertEqual(str(S(parameters=[P('foo', P.POSITIONAL_ONLY)])),
3831-
'(foo, /)')
3846+
test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
3847+
P('bar', P.VAR_KEYWORD)])
3848+
self.assertEqual(str(test), '(foo, /, **bar)')
3849+
self.assertEqual(str(test), test.format())
38323850

3833-
self.assertEqual(str(S(parameters=[
3834-
P('foo', P.POSITIONAL_ONLY),
3835-
P('bar', P.VAR_KEYWORD)])),
3836-
'(foo, /, **bar)')
3851+
test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
3852+
P('bar', P.VAR_POSITIONAL)])
3853+
self.assertEqual(str(test), '(foo, /, *bar)')
3854+
self.assertEqual(str(test), test.format())
38373855

3838-
self.assertEqual(str(S(parameters=[
3839-
P('foo', P.POSITIONAL_ONLY),
3840-
P('bar', P.VAR_POSITIONAL)])),
3841-
'(foo, /, *bar)')
3856+
def test_signature_format(self):
3857+
from typing import Annotated, Literal
3858+
3859+
def func(x: Annotated[int, 'meta'], y: Literal['a', 'b'], z: 'LiteralString'):
3860+
pass
3861+
3862+
expected_singleline = "(x: Annotated[int, 'meta'], y: Literal['a', 'b'], z: 'LiteralString')"
3863+
expected_multiline = """(
3864+
x: Annotated[int, 'meta'],
3865+
y: Literal['a', 'b'],
3866+
z: 'LiteralString'
3867+
)"""
3868+
self.assertEqual(
3869+
inspect.signature(func).format(),
3870+
expected_singleline,
3871+
)
3872+
self.assertEqual(
3873+
inspect.signature(func).format(max_width=None),
3874+
expected_singleline,
3875+
)
3876+
self.assertEqual(
3877+
inspect.signature(func).format(max_width=len(expected_singleline)),
3878+
expected_singleline,
3879+
)
3880+
self.assertEqual(
3881+
inspect.signature(func).format(max_width=len(expected_singleline) - 1),
3882+
expected_multiline,
3883+
)
3884+
self.assertEqual(
3885+
inspect.signature(func).format(max_width=0),
3886+
expected_multiline,
3887+
)
3888+
self.assertEqual(
3889+
inspect.signature(func).format(max_width=-1),
3890+
expected_multiline,
3891+
)
3892+
3893+
def test_signature_format_all_arg_types(self):
3894+
from typing import Annotated, Literal
3895+
3896+
def func(
3897+
x: Annotated[int, 'meta'],
3898+
/,
3899+
y: Literal['a', 'b'],
3900+
*,
3901+
z: 'LiteralString',
3902+
**kwargs: object,
3903+
) -> None:
3904+
pass
3905+
3906+
expected_multiline = """(
3907+
x: Annotated[int, 'meta'],
3908+
/,
3909+
y: Literal['a', 'b'],
3910+
*,
3911+
z: 'LiteralString',
3912+
**kwargs: object
3913+
) -> None"""
3914+
self.assertEqual(
3915+
inspect.signature(func).format(max_width=-1),
3916+
expected_multiline,
3917+
)
38423918

38433919
def test_signature_replace_parameters(self):
38443920
def test(a, b) -> 42:

Lib/test/test_pydoc.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,95 @@ class B(A)
870870
for expected_line in expected_lines:
871871
self.assertIn(expected_line, as_text)
872872

873+
def test_long_signatures(self):
874+
from collections.abc import Callable
875+
from typing import Literal, Annotated
876+
877+
class A:
878+
def __init__(self,
879+
arg1: Callable[[int, int, int], str],
880+
arg2: Literal['some value', 'other value'],
881+
arg3: Annotated[int, 'some docs about this type'],
882+
) -> None:
883+
...
884+
885+
doc = pydoc.render_doc(A)
886+
# clean up the extra text formatting that pydoc performs
887+
doc = re.sub('\b.', '', doc)
888+
self.assertEqual(doc, '''Python Library Documentation: class A in module %s
889+
890+
class A(builtins.object)
891+
| A(
892+
| arg1: collections.abc.Callable[[int, int, int], str],
893+
| arg2: Literal['some value', 'other value'],
894+
| arg3: Annotated[int, 'some docs about this type']
895+
| ) -> None
896+
|
897+
| Methods defined here:
898+
|
899+
| __init__(
900+
| self,
901+
| arg1: collections.abc.Callable[[int, int, int], str],
902+
| arg2: Literal['some value', 'other value'],
903+
| arg3: Annotated[int, 'some docs about this type']
904+
| ) -> None
905+
|
906+
| ----------------------------------------------------------------------
907+
| Data descriptors defined here:
908+
|
909+
| __dict__
910+
| dictionary for instance variables
911+
|
912+
| __weakref__
913+
| list of weak references to the object
914+
''' % __name__)
915+
916+
def func(
917+
arg1: Callable[[Annotated[int, 'Some doc']], str],
918+
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8],
919+
) -> Annotated[int, 'Some other']:
920+
...
921+
922+
doc = pydoc.render_doc(func)
923+
# clean up the extra text formatting that pydoc performs
924+
doc = re.sub('\b.', '', doc)
925+
self.assertEqual(doc, '''Python Library Documentation: function func in module %s
926+
927+
func(
928+
arg1: collections.abc.Callable[[typing.Annotated[int, 'Some doc']], str],
929+
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8]
930+
) -> Annotated[int, 'Some other']
931+
''' % __name__)
932+
933+
def function_with_really_long_name_so_annotations_can_be_rather_small(
934+
arg1: int,
935+
arg2: str,
936+
):
937+
...
938+
939+
doc = pydoc.render_doc(function_with_really_long_name_so_annotations_can_be_rather_small)
940+
# clean up the extra text formatting that pydoc performs
941+
doc = re.sub('\b.', '', doc)
942+
self.assertEqual(doc, '''Python Library Documentation: function function_with_really_long_name_so_annotations_can_be_rather_small in module %s
943+
944+
function_with_really_long_name_so_annotations_can_be_rather_small(
945+
arg1: int,
946+
arg2: str
947+
)
948+
''' % __name__)
949+
950+
does_not_have_name = lambda \
951+
very_long_parameter_name_that_should_not_fit_into_a_single_line, \
952+
second_very_long_parameter_name: ...
953+
954+
doc = pydoc.render_doc(does_not_have_name)
955+
# clean up the extra text formatting that pydoc performs
956+
doc = re.sub('\b.', '', doc)
957+
self.assertEqual(doc, '''Python Library Documentation: function <lambda> in module %s
958+
959+
<lambda> lambda very_long_parameter_name_that_should_not_fit_into_a_single_line, second_very_long_parameter_name
960+
''' % __name__)
961+
873962
def test__future__imports(self):
874963
# __future__ features are excluded from module help,
875964
# except when it's the __future__ module itself
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :meth:`Signature.format` to format signatures to string with extra options.
2+
And use it in :mod:`pydoc` to render more readable signatures that have new
3+
lines between parameters.

0 commit comments

Comments
 (0)