summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2024-10-09 05:03:53 (GMT)
committerGitHub <noreply@github.com>2024-10-09 05:03:53 (GMT)
commit78406382c97207b985b5c1d24db244ec398b7e3f (patch)
tree73fe961cf1ae5c751a462c5685ce97fa787378e8
parentb502573f7f800dbb2e401fa2a7a05eceac692c7a (diff)
downloadcpython-78406382c97207b985b5c1d24db244ec398b7e3f.zip
cpython-78406382c97207b985b5c1d24db244ec398b7e3f.tar.gz
cpython-78406382c97207b985b5c1d24db244ec398b7e3f.tar.bz2
gh-101552: Allow pydoc to display signatures in source format (#124669)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
-rw-r--r--Doc/library/inspect.rst25
-rw-r--r--Doc/whatsnew/3.14.rst20
-rw-r--r--Lib/inspect.py52
-rw-r--r--Lib/pydoc.py5
-rw-r--r--Lib/test/test_inspect/inspect_deferred_annotations.py2
-rw-r--r--Lib/test/test_inspect/test_inspect.py35
-rw-r--r--Lib/test/test_pydoc/test_pydoc.py10
-rw-r--r--Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst4
8 files changed, 126 insertions, 27 deletions
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 8536718..1eaf1cc 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -694,7 +694,7 @@ and its return annotation. To retrieve a :class:`!Signature` object,
use the :func:`!signature`
function.
-.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
+.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False, annotation_format=Format.VALUE)
Return a :class:`Signature` object for the given *callable*:
@@ -725,7 +725,12 @@ function.
*globals*, *locals*, and *eval_str* parameters are passed
into :func:`!annotationlib.get_annotations` when resolving the
annotations; see the documentation for :func:`!annotationlib.get_annotations`
- for instructions on how to use these parameters.
+ for instructions on how to use these parameters. A member of the
+ :class:`annotationlib.Format` enum can be passed to the
+ *annotation_format* parameter to control the format of the returned
+ annotations. For example, use
+ ``annotation_format=annotationlib.Format.STRING`` to return annotations in string
+ format.
Raises :exc:`ValueError` if no signature can be provided, and
:exc:`TypeError` if that type of object is not supported. Also,
@@ -733,7 +738,7 @@ function.
the ``eval()`` call(s) to un-stringize the annotations in :func:`annotationlib.get_annotations`
could potentially raise any kind of exception.
- A slash(/) in the signature of a function denotes that the parameters prior
+ A slash (/) in the signature of a function denotes that the parameters prior
to it are positional-only. For more info, see
:ref:`the FAQ entry on positional-only parameters <faq-positional-only-arguments>`.
@@ -746,6 +751,9 @@ function.
.. versionchanged:: 3.10
The *globals*, *locals*, and *eval_str* parameters were added.
+ .. versionchanged:: 3.14
+ The *annotation_format* parameter was added.
+
.. note::
Some callables may not be introspectable in certain implementations of
@@ -838,7 +846,7 @@ function.
:class:`Signature` objects are also supported by the generic function
:func:`copy.replace`.
- .. method:: format(*, max_width=None)
+ .. method:: format(*, max_width=None, quote_annotation_strings=True)
Create a string representation of the :class:`Signature` object.
@@ -847,8 +855,17 @@ function.
If the signature is longer than *max_width*,
all parameters will be on separate lines.
+ If *quote_annotation_strings* is False, :term:`annotations <annotation>`
+ in the signature are displayed without opening and closing quotation
+ marks if they are strings. This is useful if the signature was created with the
+ :attr:`~annotationlib.Format.STRING` format or if
+ ``from __future__ import annotations`` was used.
+
.. versionadded:: 3.13
+ .. versionchanged:: 3.14
+ The *unquote_annotations* parameter was added.
+
.. 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/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 4d71a24..c62a3ca 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -281,6 +281,18 @@ http
(Contributed by Yorik Hansen in :gh:`123430`.)
+inspect
+-------
+
+* :func:`inspect.signature` takes a new argument *annotation_format* to control
+ the :class:`annotationlib.Format` used for representing annotations.
+ (Contributed by Jelle Zijlstra in :gh:`101552`.)
+
+* :meth:`inspect.Signature.format` takes a new argument *unquote_annotations*.
+ If true, string :term:`annotations <annotation>` are displayed without surrounding quotes.
+ (Contributed by Jelle Zijlstra in :gh:`101552`.)
+
+
json
----
@@ -356,6 +368,14 @@ pickle
of the error.
(Contributed by Serhiy Storchaka in :gh:`122213`.)
+pydoc
+-----
+
+* :term:`Annotations <annotation>` in help output are now usually
+ displayed in a format closer to that in the original source.
+ (Contributed by Jelle Zijlstra in :gh:`101552`.)
+
+
symtable
--------
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 1763ef6..0c33c6c 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -140,6 +140,7 @@ __all__ = [
import abc
+from annotationlib import Format
from annotationlib import get_annotations # re-exported
import ast
import dis
@@ -1319,7 +1320,9 @@ def getargvalues(frame):
args, varargs, varkw = getargs(frame.f_code)
return ArgInfo(args, varargs, varkw, frame.f_locals)
-def formatannotation(annotation, base_module=None):
+def formatannotation(annotation, base_module=None, *, quote_annotation_strings=True):
+ if not quote_annotation_strings and isinstance(annotation, str):
+ return annotation
if getattr(annotation, '__module__', None) == 'typing':
def repl(match):
text = match.group()
@@ -2270,7 +2273,8 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True):
def _signature_from_function(cls, func, skip_bound_arg=True,
- globals=None, locals=None, eval_str=False):
+ globals=None, locals=None, eval_str=False,
+ *, annotation_format=Format.VALUE):
"""Private helper: constructs Signature for the given python function."""
is_duck_function = False
@@ -2296,7 +2300,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
positional = arg_names[:pos_count]
keyword_only_count = func_code.co_kwonlyargcount
keyword_only = arg_names[pos_count:pos_count + keyword_only_count]
- annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str)
+ annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
+ format=annotation_format)
defaults = func.__defaults__
kwdefaults = func.__kwdefaults__
@@ -2379,7 +2384,8 @@ def _signature_from_callable(obj, *,
globals=None,
locals=None,
eval_str=False,
- sigcls):
+ sigcls,
+ annotation_format=Format.VALUE):
"""Private helper function to get signature for arbitrary
callable objects.
@@ -2391,7 +2397,8 @@ def _signature_from_callable(obj, *,
globals=globals,
locals=locals,
sigcls=sigcls,
- eval_str=eval_str)
+ eval_str=eval_str,
+ annotation_format=annotation_format)
if not callable(obj):
raise TypeError('{!r} is not a callable object'.format(obj))
@@ -2472,7 +2479,8 @@ def _signature_from_callable(obj, *,
# of a Python function (Cython functions, for instance), then:
return _signature_from_function(sigcls, obj,
skip_bound_arg=skip_bound_arg,
- globals=globals, locals=locals, eval_str=eval_str)
+ globals=globals, locals=locals, eval_str=eval_str,
+ annotation_format=annotation_format)
if _signature_is_builtin(obj):
return _signature_from_builtin(sigcls, obj,
@@ -2707,13 +2715,17 @@ class Parameter:
return type(self)(name, kind, default=default, annotation=annotation)
def __str__(self):
+ return self._format()
+
+ def _format(self, *, quote_annotation_strings=True):
kind = self.kind
formatted = self._name
# Add annotation and default value
if self._annotation is not _empty:
- formatted = '{}: {}'.format(formatted,
- formatannotation(self._annotation))
+ annotation = formatannotation(self._annotation,
+ quote_annotation_strings=quote_annotation_strings)
+ formatted = '{}: {}'.format(formatted, annotation)
if self._default is not _empty:
if self._annotation is not _empty:
@@ -2961,11 +2973,13 @@ class Signature:
@classmethod
def from_callable(cls, obj, *,
- follow_wrapped=True, globals=None, locals=None, eval_str=False):
+ follow_wrapped=True, globals=None, locals=None, eval_str=False,
+ annotation_format=Format.VALUE):
"""Constructs Signature for the given callable object."""
return _signature_from_callable(obj, sigcls=cls,
follow_wrapper_chains=follow_wrapped,
- globals=globals, locals=locals, eval_str=eval_str)
+ globals=globals, locals=locals, eval_str=eval_str,
+ annotation_format=annotation_format)
@property
def parameters(self):
@@ -3180,19 +3194,24 @@ class Signature:
def __str__(self):
return self.format()
- def format(self, *, max_width=None):
+ def format(self, *, max_width=None, quote_annotation_strings=True):
"""Create a string representation of the Signature object.
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.
+
+ If *quote_annotation_strings* is False, annotations
+ in the signature are displayed without opening and closing quotation
+ marks. This is useful when the signature was created with the
+ STRING format or when ``from __future__ import annotations`` was used.
"""
result = []
render_pos_only_separator = False
render_kw_only_separator = True
for param in self.parameters.values():
- formatted = str(param)
+ formatted = param._format(quote_annotation_strings=quote_annotation_strings)
kind = param.kind
@@ -3229,16 +3248,19 @@ class Signature:
rendered = '(\n {}\n)'.format(',\n '.join(result))
if self.return_annotation is not _empty:
- anno = formatannotation(self.return_annotation)
+ anno = formatannotation(self.return_annotation,
+ quote_annotation_strings=quote_annotation_strings)
rendered += ' -> {}'.format(anno)
return rendered
-def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
+def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False,
+ annotation_format=Format.VALUE):
"""Get a signature object for the passed callable."""
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
- globals=globals, locals=locals, eval_str=eval_str)
+ globals=globals, locals=locals, eval_str=eval_str,
+ annotation_format=annotation_format)
class BufferFlags(enum.IntFlag):
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index eec7b07..c863794 100644
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -71,6 +71,7 @@ import time
import tokenize
import urllib.parse
import warnings
+from annotationlib import Format
from collections import deque
from reprlib import Repr
from traceback import format_exception_only
@@ -212,12 +213,12 @@ def splitdoc(doc):
def _getargspec(object):
try:
- signature = inspect.signature(object)
+ signature = inspect.signature(object, annotation_format=Format.STRING)
if 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)
+ return signature.format(max_width=max_width, quote_annotation_strings=False)
except (ValueError, TypeError):
argspec = getattr(object, '__text_signature__', None)
if argspec:
diff --git a/Lib/test/test_inspect/inspect_deferred_annotations.py b/Lib/test/test_inspect/inspect_deferred_annotations.py
new file mode 100644
index 0000000..bb59ef1
--- /dev/null
+++ b/Lib/test/test_inspect/inspect_deferred_annotations.py
@@ -0,0 +1,2 @@
+def f(x: undefined):
+ pass
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index 2ecb7ec..9fa6d23 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -1,3 +1,4 @@
+from annotationlib import Format, ForwardRef
import asyncio
import builtins
import collections
@@ -22,7 +23,6 @@ import time
import types
import tempfile
import textwrap
-from typing import Unpack
import unicodedata
import unittest
import unittest.mock
@@ -46,6 +46,7 @@ from test import support
from test.test_inspect import inspect_fodder as mod
from test.test_inspect import inspect_fodder2 as mod2
from test.test_inspect import inspect_stringized_annotations
+from test.test_inspect import inspect_deferred_annotations
# Functions tested in this suite:
@@ -4622,6 +4623,18 @@ class TestSignatureObject(unittest.TestCase):
expected_multiline,
)
+ def test_signature_format_unquote(self):
+ def func(x: 'int') -> 'str': ...
+
+ self.assertEqual(
+ inspect.signature(func).format(),
+ "(x: 'int') -> 'str'"
+ )
+ self.assertEqual(
+ inspect.signature(func).format(quote_annotation_strings=False),
+ "(x: int) -> str"
+ )
+
def test_signature_replace_parameters(self):
def test(a, b) -> 42:
pass
@@ -4854,6 +4867,26 @@ class TestSignatureObject(unittest.TestCase):
par('b', PORK, annotation=tuple),
)))
+ def test_signature_annotation_format(self):
+ ida = inspect_deferred_annotations
+ sig = inspect.Signature
+ par = inspect.Parameter
+ PORK = inspect.Parameter.POSITIONAL_OR_KEYWORD
+ for signature_func in (inspect.signature, inspect.Signature.from_callable):
+ with self.subTest(signature_func=signature_func):
+ self.assertEqual(
+ signature_func(ida.f, annotation_format=Format.STRING),
+ sig([par("x", PORK, annotation="undefined")])
+ )
+ self.assertEqual(
+ signature_func(ida.f, annotation_format=Format.FORWARDREF),
+ sig([par("x", PORK, annotation=ForwardRef("undefined"))])
+ )
+ with self.assertRaisesRegex(NameError, "undefined"):
+ signature_func(ida.f, annotation_format=Format.VALUE)
+ with self.assertRaisesRegex(NameError, "undefined"):
+ signature_func(ida.f)
+
def test_signature_none_annotation(self):
class funclike:
# Has to be callable, and have correct
diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py
index 776e02f..2a4d3ab 100644
--- a/Lib/test/test_pydoc/test_pydoc.py
+++ b/Lib/test/test_pydoc/test_pydoc.py
@@ -1073,7 +1073,7 @@ class B(A)
class A(builtins.object)
| A(
- | arg1: collections.abc.Callable[[int, int, int], str],
+ | arg1: Callable[[int, int, int], str],
| arg2: Literal['some value', 'other value'],
| arg3: Annotated[int, 'some docs about this type']
| ) -> None
@@ -1082,7 +1082,7 @@ class A(builtins.object)
|
| __init__(
| self,
- | arg1: collections.abc.Callable[[int, int, int], str],
+ | arg1: Callable[[int, int, int], str],
| arg2: Literal['some value', 'other value'],
| arg3: Annotated[int, 'some docs about this type']
| ) -> None
@@ -1109,7 +1109,7 @@ class A(builtins.object)
self.assertEqual(doc, '''Python Library Documentation: function func in module %s
func(
- arg1: collections.abc.Callable[[typing.Annotated[int, 'Some doc']], str],
+ arg1: Callable[[Annotated[int, 'Some doc']], str],
arg2: Literal[1, 2, 3, 4, 5, 6, 7, 8]
) -> Annotated[int, 'Some other']
''' % __name__)
@@ -1394,8 +1394,8 @@ class TestDescriptions(unittest.TestCase):
T = typing.TypeVar('T')
class C(typing.Generic[T], typing.Mapping[int, str]): ...
self.assertEqual(pydoc.render_doc(foo).splitlines()[-1],
- 'f\x08fo\x08oo\x08o(data: List[Any], x: int)'
- ' -> Iterator[Tuple[int, Any]]')
+ 'f\x08fo\x08oo\x08o(data: typing.List[typing.Any], x: int)'
+ ' -> typing.Iterator[typing.Tuple[int, typing.Any]]')
self.assertEqual(pydoc.render_doc(C).splitlines()[2],
'class C\x08C(collections.abc.Mapping, typing.Generic)')
diff --git a/Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst b/Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst
new file mode 100644
index 0000000..913a84d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst
@@ -0,0 +1,4 @@
+Add an *annoation_format* parameter to :func:`inspect.signature`. Add an
+*quote_annotation_strings* parameter to :meth:`inspect.Signature.format`. Use the
+new functionality to improve the display of annotations in signatures in
+:mod:`pydoc`. Patch by Jelle Zijlstra.