summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/inspect.rst11
-rw-r--r--Lib/inspect.py12
-rwxr-xr-xLib/pydoc.py5
-rw-r--r--Lib/test/test_inspect/test_inspect.py96
-rw-r--r--Lib/test/test_pydoc.py89
-rw-r--r--Misc/NEWS.d/next/Library/2023-11-16-10-42-15.gh-issue-112139.WpHosf.rst3
6 files changed, 205 insertions, 11 deletions
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 94c5d1c..0852251 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -753,6 +753,17 @@ function.
Signature objects are also supported by generic function
: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)
Return a :class:`Signature` (or its subclass) object for a given callable
diff --git a/Lib/inspect.py b/Lib/inspect.py
index aaa22be..079385a 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -3316,6 +3316,16 @@ class Signature:
return '<{} {}>'.format(self.__class__.__name__, 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 = []
render_pos_only_separator = False
render_kw_only_separator = True
@@ -3353,6 +3363,8 @@ class Signature:
result.append('/')
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:
anno = formatannotation(self.return_annotation)
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index be41592..83c74a7 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -201,7 +201,10 @@ def _getargspec(object):
try:
signature = inspect.signature(object)
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):
argspec = getattr(object, '__text_signature__', None)
if argspec:
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index e75682f..09d5085 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -3796,26 +3796,36 @@ class TestSignatureObject(unittest.TestCase):
pass
self.assertEqual(str(inspect.signature(foo)),
'(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:
pass
self.assertEqual(str(inspect.signature(foo)),
'(a: int = 1, *args, b, c=None, **kwargs) -> 42')
+ self.assertEqual(str(inspect.signature(foo)),
+ inspect.signature(foo).format())
def foo():
pass
self.assertEqual(str(inspect.signature(foo)), '()')
+ self.assertEqual(str(inspect.signature(foo)),
+ inspect.signature(foo).format())
def foo(a: list[str]) -> tuple[str, float]:
pass
self.assertEqual(str(inspect.signature(foo)),
'(a: list[str]) -> tuple[str, float]')
+ self.assertEqual(str(inspect.signature(foo)),
+ inspect.signature(foo).format())
from typing import Tuple
def foo(a: list[str]) -> Tuple[str, float]:
pass
self.assertEqual(str(inspect.signature(foo)),
'(a: list[str]) -> Tuple[str, float]')
+ self.assertEqual(str(inspect.signature(foo)),
+ inspect.signature(foo).format())
def test_signature_str_positional_only(self):
P = inspect.Parameter
@@ -3826,19 +3836,85 @@ class TestSignatureObject(unittest.TestCase):
self.assertEqual(str(inspect.signature(test)),
'(a_po, /, *, b, **kwargs)')
+ self.assertEqual(str(inspect.signature(test)),
+ inspect.signature(test).format())
+
+ test = S(parameters=[P('foo', P.POSITIONAL_ONLY)])
+ self.assertEqual(str(test), '(foo, /)')
+ self.assertEqual(str(test), test.format())
- self.assertEqual(str(S(parameters=[P('foo', P.POSITIONAL_ONLY)])),
- '(foo, /)')
+ test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
+ P('bar', P.VAR_KEYWORD)])
+ self.assertEqual(str(test), '(foo, /, **bar)')
+ self.assertEqual(str(test), test.format())
- self.assertEqual(str(S(parameters=[
- P('foo', P.POSITIONAL_ONLY),
- P('bar', P.VAR_KEYWORD)])),
- '(foo, /, **bar)')
+ test = S(parameters=[P('foo', P.POSITIONAL_ONLY),
+ P('bar', P.VAR_POSITIONAL)])
+ self.assertEqual(str(test), '(foo, /, *bar)')
+ self.assertEqual(str(test), test.format())
- self.assertEqual(str(S(parameters=[
- P('foo', P.POSITIONAL_ONLY),
- P('bar', P.VAR_POSITIONAL)])),
- '(foo, /, *bar)')
+ 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(a, b) -> 42:
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 745717f..eb50510 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -870,6 +870,95 @@ class B(A)
for expected_line in expected_lines:
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):
# __future__ features are excluded from module help,
# except when it's the __future__ module itself
diff --git a/Misc/NEWS.d/next/Library/2023-11-16-10-42-15.gh-issue-112139.WpHosf.rst b/Misc/NEWS.d/next/Library/2023-11-16-10-42-15.gh-issue-112139.WpHosf.rst
new file mode 100644
index 0000000..090dc88
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-11-16-10-42-15.gh-issue-112139.WpHosf.rst
@@ -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.