Skip to content

Commit 7840638

Browse files
gh-101552: Allow pydoc to display signatures in source format (#124669)
Co-authored-by: Alex Waygood <[email protected]>
1 parent b502573 commit 7840638

File tree

8 files changed

+126
-27
lines changed

8 files changed

+126
-27
lines changed

Doc/library/inspect.rst

+21-4
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ and its return annotation. To retrieve a :class:`!Signature` object,
694694
use the :func:`!signature`
695695
function.
696696

697-
.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
697+
.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False, annotation_format=Format.VALUE)
698698

699699
Return a :class:`Signature` object for the given *callable*:
700700

@@ -725,15 +725,20 @@ function.
725725
*globals*, *locals*, and *eval_str* parameters are passed
726726
into :func:`!annotationlib.get_annotations` when resolving the
727727
annotations; see the documentation for :func:`!annotationlib.get_annotations`
728-
for instructions on how to use these parameters.
728+
for instructions on how to use these parameters. A member of the
729+
:class:`annotationlib.Format` enum can be passed to the
730+
*annotation_format* parameter to control the format of the returned
731+
annotations. For example, use
732+
``annotation_format=annotationlib.Format.STRING`` to return annotations in string
733+
format.
729734

730735
Raises :exc:`ValueError` if no signature can be provided, and
731736
:exc:`TypeError` if that type of object is not supported. Also,
732737
if the annotations are stringized, and *eval_str* is not false,
733738
the ``eval()`` call(s) to un-stringize the annotations in :func:`annotationlib.get_annotations`
734739
could potentially raise any kind of exception.
735740

736-
A slash(/) in the signature of a function denotes that the parameters prior
741+
A slash (/) in the signature of a function denotes that the parameters prior
737742
to it are positional-only. For more info, see
738743
:ref:`the FAQ entry on positional-only parameters <faq-positional-only-arguments>`.
739744

@@ -746,6 +751,9 @@ function.
746751
.. versionchanged:: 3.10
747752
The *globals*, *locals*, and *eval_str* parameters were added.
748753

754+
.. versionchanged:: 3.14
755+
The *annotation_format* parameter was added.
756+
749757
.. note::
750758

751759
Some callables may not be introspectable in certain implementations of
@@ -838,7 +846,7 @@ function.
838846
:class:`Signature` objects are also supported by the generic function
839847
:func:`copy.replace`.
840848

841-
.. method:: format(*, max_width=None)
849+
.. method:: format(*, max_width=None, quote_annotation_strings=True)
842850

843851
Create a string representation of the :class:`Signature` object.
844852

@@ -847,8 +855,17 @@ function.
847855
If the signature is longer than *max_width*,
848856
all parameters will be on separate lines.
849857

858+
If *quote_annotation_strings* is False, :term:`annotations <annotation>`
859+
in the signature are displayed without opening and closing quotation
860+
marks if they are strings. This is useful if the signature was created with the
861+
:attr:`~annotationlib.Format.STRING` format or if
862+
``from __future__ import annotations`` was used.
863+
850864
.. versionadded:: 3.13
851865

866+
.. versionchanged:: 3.14
867+
The *unquote_annotations* parameter was added.
868+
852869
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
853870

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

Doc/whatsnew/3.14.rst

+20
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ http
281281
(Contributed by Yorik Hansen in :gh:`123430`.)
282282

283283

284+
inspect
285+
-------
286+
287+
* :func:`inspect.signature` takes a new argument *annotation_format* to control
288+
the :class:`annotationlib.Format` used for representing annotations.
289+
(Contributed by Jelle Zijlstra in :gh:`101552`.)
290+
291+
* :meth:`inspect.Signature.format` takes a new argument *unquote_annotations*.
292+
If true, string :term:`annotations <annotation>` are displayed without surrounding quotes.
293+
(Contributed by Jelle Zijlstra in :gh:`101552`.)
294+
295+
284296
json
285297
----
286298

@@ -356,6 +368,14 @@ pickle
356368
of the error.
357369
(Contributed by Serhiy Storchaka in :gh:`122213`.)
358370

371+
pydoc
372+
-----
373+
374+
* :term:`Annotations <annotation>` in help output are now usually
375+
displayed in a format closer to that in the original source.
376+
(Contributed by Jelle Zijlstra in :gh:`101552`.)
377+
378+
359379
symtable
360380
--------
361381

Lib/inspect.py

+37-15
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140

141141

142142
import abc
143+
from annotationlib import Format
143144
from annotationlib import get_annotations # re-exported
144145
import ast
145146
import dis
@@ -1319,7 +1320,9 @@ def getargvalues(frame):
13191320
args, varargs, varkw = getargs(frame.f_code)
13201321
return ArgInfo(args, varargs, varkw, frame.f_locals)
13211322

1322-
def formatannotation(annotation, base_module=None):
1323+
def formatannotation(annotation, base_module=None, *, quote_annotation_strings=True):
1324+
if not quote_annotation_strings and isinstance(annotation, str):
1325+
return annotation
13231326
if getattr(annotation, '__module__', None) == 'typing':
13241327
def repl(match):
13251328
text = match.group()
@@ -2270,7 +2273,8 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True):
22702273

22712274

22722275
def _signature_from_function(cls, func, skip_bound_arg=True,
2273-
globals=None, locals=None, eval_str=False):
2276+
globals=None, locals=None, eval_str=False,
2277+
*, annotation_format=Format.VALUE):
22742278
"""Private helper: constructs Signature for the given python function."""
22752279

22762280
is_duck_function = False
@@ -2296,7 +2300,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
22962300
positional = arg_names[:pos_count]
22972301
keyword_only_count = func_code.co_kwonlyargcount
22982302
keyword_only = arg_names[pos_count:pos_count + keyword_only_count]
2299-
annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str)
2303+
annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
2304+
format=annotation_format)
23002305
defaults = func.__defaults__
23012306
kwdefaults = func.__kwdefaults__
23022307

@@ -2379,7 +2384,8 @@ def _signature_from_callable(obj, *,
23792384
globals=None,
23802385
locals=None,
23812386
eval_str=False,
2382-
sigcls):
2387+
sigcls,
2388+
annotation_format=Format.VALUE):
23832389

23842390
"""Private helper function to get signature for arbitrary
23852391
callable objects.
@@ -2391,7 +2397,8 @@ def _signature_from_callable(obj, *,
23912397
globals=globals,
23922398
locals=locals,
23932399
sigcls=sigcls,
2394-
eval_str=eval_str)
2400+
eval_str=eval_str,
2401+
annotation_format=annotation_format)
23952402

23962403
if not callable(obj):
23972404
raise TypeError('{!r} is not a callable object'.format(obj))
@@ -2472,7 +2479,8 @@ def _signature_from_callable(obj, *,
24722479
# of a Python function (Cython functions, for instance), then:
24732480
return _signature_from_function(sigcls, obj,
24742481
skip_bound_arg=skip_bound_arg,
2475-
globals=globals, locals=locals, eval_str=eval_str)
2482+
globals=globals, locals=locals, eval_str=eval_str,
2483+
annotation_format=annotation_format)
24762484

24772485
if _signature_is_builtin(obj):
24782486
return _signature_from_builtin(sigcls, obj,
@@ -2707,13 +2715,17 @@ def replace(self, *, name=_void, kind=_void,
27072715
return type(self)(name, kind, default=default, annotation=annotation)
27082716

27092717
def __str__(self):
2718+
return self._format()
2719+
2720+
def _format(self, *, quote_annotation_strings=True):
27102721
kind = self.kind
27112722
formatted = self._name
27122723

27132724
# Add annotation and default value
27142725
if self._annotation is not _empty:
2715-
formatted = '{}: {}'.format(formatted,
2716-
formatannotation(self._annotation))
2726+
annotation = formatannotation(self._annotation,
2727+
quote_annotation_strings=quote_annotation_strings)
2728+
formatted = '{}: {}'.format(formatted, annotation)
27172729

27182730
if self._default is not _empty:
27192731
if self._annotation is not _empty:
@@ -2961,11 +2973,13 @@ def __init__(self, parameters=None, *, return_annotation=_empty,
29612973

29622974
@classmethod
29632975
def from_callable(cls, obj, *,
2964-
follow_wrapped=True, globals=None, locals=None, eval_str=False):
2976+
follow_wrapped=True, globals=None, locals=None, eval_str=False,
2977+
annotation_format=Format.VALUE):
29652978
"""Constructs Signature for the given callable object."""
29662979
return _signature_from_callable(obj, sigcls=cls,
29672980
follow_wrapper_chains=follow_wrapped,
2968-
globals=globals, locals=locals, eval_str=eval_str)
2981+
globals=globals, locals=locals, eval_str=eval_str,
2982+
annotation_format=annotation_format)
29692983

29702984
@property
29712985
def parameters(self):
@@ -3180,19 +3194,24 @@ def __repr__(self):
31803194
def __str__(self):
31813195
return self.format()
31823196

3183-
def format(self, *, max_width=None):
3197+
def format(self, *, max_width=None, quote_annotation_strings=True):
31843198
"""Create a string representation of the Signature object.
31853199
31863200
If *max_width* integer is passed,
31873201
signature will try to fit into the *max_width*.
31883202
If signature is longer than *max_width*,
31893203
all parameters will be on separate lines.
3204+
3205+
If *quote_annotation_strings* is False, annotations
3206+
in the signature are displayed without opening and closing quotation
3207+
marks. This is useful when the signature was created with the
3208+
STRING format or when ``from __future__ import annotations`` was used.
31903209
"""
31913210
result = []
31923211
render_pos_only_separator = False
31933212
render_kw_only_separator = True
31943213
for param in self.parameters.values():
3195-
formatted = str(param)
3214+
formatted = param._format(quote_annotation_strings=quote_annotation_strings)
31963215

31973216
kind = param.kind
31983217

@@ -3229,16 +3248,19 @@ def format(self, *, max_width=None):
32293248
rendered = '(\n {}\n)'.format(',\n '.join(result))
32303249

32313250
if self.return_annotation is not _empty:
3232-
anno = formatannotation(self.return_annotation)
3251+
anno = formatannotation(self.return_annotation,
3252+
quote_annotation_strings=quote_annotation_strings)
32333253
rendered += ' -> {}'.format(anno)
32343254

32353255
return rendered
32363256

32373257

3238-
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
3258+
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False,
3259+
annotation_format=Format.VALUE):
32393260
"""Get a signature object for the passed callable."""
32403261
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
3241-
globals=globals, locals=locals, eval_str=eval_str)
3262+
globals=globals, locals=locals, eval_str=eval_str,
3263+
annotation_format=annotation_format)
32423264

32433265

32443266
class BufferFlags(enum.IntFlag):

Lib/pydoc.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class or function within a module or module in a package. If the
7171
import tokenize
7272
import urllib.parse
7373
import warnings
74+
from annotationlib import Format
7475
from collections import deque
7576
from reprlib import Repr
7677
from traceback import format_exception_only
@@ -212,12 +213,12 @@ def splitdoc(doc):
212213

213214
def _getargspec(object):
214215
try:
215-
signature = inspect.signature(object)
216+
signature = inspect.signature(object, annotation_format=Format.STRING)
216217
if signature:
217218
name = getattr(object, '__name__', '')
218219
# <lambda> function are always single-line and should not be formatted
219220
max_width = (80 - len(name)) if name != '<lambda>' else None
220-
return signature.format(max_width=max_width)
221+
return signature.format(max_width=max_width, quote_annotation_strings=False)
221222
except (ValueError, TypeError):
222223
argspec = getattr(object, '__text_signature__', None)
223224
if argspec:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def f(x: undefined):
2+
pass

Lib/test/test_inspect/test_inspect.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from annotationlib import Format, ForwardRef
12
import asyncio
23
import builtins
34
import collections
@@ -22,7 +23,6 @@
2223
import types
2324
import tempfile
2425
import textwrap
25-
from typing import Unpack
2626
import unicodedata
2727
import unittest
2828
import unittest.mock
@@ -46,6 +46,7 @@
4646
from test.test_inspect import inspect_fodder as mod
4747
from test.test_inspect import inspect_fodder2 as mod2
4848
from test.test_inspect import inspect_stringized_annotations
49+
from test.test_inspect import inspect_deferred_annotations
4950

5051

5152
# Functions tested in this suite:
@@ -4622,6 +4623,18 @@ def func(
46224623
expected_multiline,
46234624
)
46244625

4626+
def test_signature_format_unquote(self):
4627+
def func(x: 'int') -> 'str': ...
4628+
4629+
self.assertEqual(
4630+
inspect.signature(func).format(),
4631+
"(x: 'int') -> 'str'"
4632+
)
4633+
self.assertEqual(
4634+
inspect.signature(func).format(quote_annotation_strings=False),
4635+
"(x: int) -> str"
4636+
)
4637+
46254638
def test_signature_replace_parameters(self):
46264639
def test(a, b) -> 42:
46274640
pass
@@ -4854,6 +4867,26 @@ def test_signature_eval_str(self):
48544867
par('b', PORK, annotation=tuple),
48554868
)))
48564869

4870+
def test_signature_annotation_format(self):
4871+
ida = inspect_deferred_annotations
4872+
sig = inspect.Signature
4873+
par = inspect.Parameter
4874+
PORK = inspect.Parameter.POSITIONAL_OR_KEYWORD
4875+
for signature_func in (inspect.signature, inspect.Signature.from_callable):
4876+
with self.subTest(signature_func=signature_func):
4877+
self.assertEqual(
4878+
signature_func(ida.f, annotation_format=Format.STRING),
4879+
sig([par("x", PORK, annotation="undefined")])
4880+
)
4881+
self.assertEqual(
4882+
signature_func(ida.f, annotation_format=Format.FORWARDREF),
4883+
sig([par("x", PORK, annotation=ForwardRef("undefined"))])
4884+
)
4885+
with self.assertRaisesRegex(NameError, "undefined"):
4886+
signature_func(ida.f, annotation_format=Format.VALUE)
4887+
with self.assertRaisesRegex(NameError, "undefined"):
4888+
signature_func(ida.f)
4889+
48574890
def test_signature_none_annotation(self):
48584891
class funclike:
48594892
# Has to be callable, and have correct

Lib/test/test_pydoc/test_pydoc.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ def __init__(self,
10731073
10741074
class A(builtins.object)
10751075
| A(
1076-
| arg1: collections.abc.Callable[[int, int, int], str],
1076+
| arg1: Callable[[int, int, int], str],
10771077
| arg2: Literal['some value', 'other value'],
10781078
| arg3: Annotated[int, 'some docs about this type']
10791079
| ) -> None
@@ -1082,7 +1082,7 @@ class A(builtins.object)
10821082
|
10831083
| __init__(
10841084
| self,
1085-
| arg1: collections.abc.Callable[[int, int, int], str],
1085+
| arg1: Callable[[int, int, int], str],
10861086
| arg2: Literal['some value', 'other value'],
10871087
| arg3: Annotated[int, 'some docs about this type']
10881088
| ) -> None
@@ -1109,7 +1109,7 @@ def func(
11091109
self.assertEqual(doc, '''Python Library Documentation: function func in module %s
11101110
11111111
func(
1112-
arg1: collections.abc.Callable[[typing.Annotated[int, 'Some doc']], str],
1112+
arg1: Callable[[Annotated[int, 'Some doc']], str],
11131113
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8]
11141114
) -> Annotated[int, 'Some other']
11151115
''' % __name__)
@@ -1394,8 +1394,8 @@ def foo(data: typing.List[typing.Any],
13941394
T = typing.TypeVar('T')
13951395
class C(typing.Generic[T], typing.Mapping[int, str]): ...
13961396
self.assertEqual(pydoc.render_doc(foo).splitlines()[-1],
1397-
'f\x08fo\x08oo\x08o(data: List[Any], x: int)'
1398-
' -> Iterator[Tuple[int, Any]]')
1397+
'f\x08fo\x08oo\x08o(data: typing.List[typing.Any], x: int)'
1398+
' -> typing.Iterator[typing.Tuple[int, typing.Any]]')
13991399
self.assertEqual(pydoc.render_doc(C).splitlines()[2],
14001400
'class C\x08C(collections.abc.Mapping, typing.Generic)')
14011401

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add an *annoation_format* parameter to :func:`inspect.signature`. Add an
2+
*quote_annotation_strings* parameter to :meth:`inspect.Signature.format`. Use the
3+
new functionality to improve the display of annotations in signatures in
4+
:mod:`pydoc`. Patch by Jelle Zijlstra.

0 commit comments

Comments
 (0)