From f80679145ad1cb05f12c7b32744f9576f8182e51 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 26 Sep 2024 22:24:44 -0700 Subject: [PATCH 1/9] gh-101552: Add format argument to inspect.signature And use the STRING format in pydoc. --- Doc/library/inspect.rst | 13 ++++++++---- Doc/whatsnew/3.14.rst | 8 ++++++++ Lib/inspect.py | 29 ++++++++++++++++++--------- Lib/pydoc.py | 6 +++++- Lib/test/test_inspect/test_inspect.py | 23 ++++++++++++++++++++- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 853671856b2a14..af3df61fca6b62 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, format=Format.VALUE) Return a :class:`Signature` object for the given *callable*: @@ -722,10 +722,12 @@ function. ``from __future__ import annotations`` was used), :func:`signature` will attempt to automatically un-stringize the annotations using :func:`annotationlib.get_annotations`. The - *globals*, *locals*, and *eval_str* parameters are passed + *globals*, *locals*, *eval_str*, and *format* 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. For example, use + ``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 +735,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 `. @@ -746,6 +748,9 @@ function. .. versionchanged:: 3.10 The *globals*, *locals*, and *eval_str* parameters were added. + .. versionchanged:: 3.14 + The *format* parameter was added. + .. note:: Some callables may not be introspectable in certain implementations of diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 3d6084e6ecc19b..a2d957310a1e63 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -272,6 +272,14 @@ module allow the browser to apply its default dark mode. (Contributed by Yorik Hansen in :gh:`123430`.) +inspect +------- + +* :func:`inspect.signature` takes a new argument *format* to control + the :class:`annotationlib.Format` used for representing annotations. + (Contributed by Jelle Zijlstra in :gh:`101552`.) + + json ---- diff --git a/Lib/inspect.py b/Lib/inspect.py index 2b25300fcb2509..fac68a8e845e8d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -140,7 +140,7 @@ import abc -from annotationlib import get_annotations # re-exported +from annotationlib import Format, get_annotations # re-exported import ast import dis import collections.abc @@ -2268,7 +2268,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, + format=Format.VALUE): """Private helper: constructs Signature for the given python function.""" is_duck_function = False @@ -2294,7 +2295,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=format) defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ @@ -2377,7 +2379,8 @@ def _signature_from_callable(obj, *, globals=None, locals=None, eval_str=False, - sigcls): + sigcls, + format=Format.VALUE): """Private helper function to get signature for arbitrary callable objects. @@ -2389,7 +2392,8 @@ def _signature_from_callable(obj, *, globals=globals, locals=locals, sigcls=sigcls, - eval_str=eval_str) + eval_str=eval_str, + format=format) if not callable(obj): raise TypeError('{!r} is not a callable object'.format(obj)) @@ -2478,7 +2482,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, + format=format) if _signature_is_builtin(obj): return _signature_from_builtin(sigcls, obj, @@ -2967,11 +2972,13 @@ def __init__(self, parameters=None, *, return_annotation=_empty, @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, + 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, + format=format) @property def parameters(self): @@ -3241,10 +3248,12 @@ def format(self, *, max_width=None): 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, + 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, + format=format) class BufferFlags(enum.IntFlag): diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d376592d69d40d..e4b68f796631d5 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -71,6 +71,7 @@ class or function within a module or module in a package. If the 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,13 +213,16 @@ def splitdoc(doc): def _getargspec(object): try: - signature = inspect.signature(object) + signature = inspect.signature(object, format=Format.STRING) if signature: name = getattr(object, '__name__', '') # function are always single-line and should not be formatted max_width = (80 - len(name)) if name != '' else None return signature.format(max_width=max_width) except (ValueError, TypeError): + import traceback + traceback.print_exc() + raise argspec = getattr(object, '__text_signature__', None) if argspec: if argspec[:2] == '($': diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index aeee504fb8b555..6d6a6627508a80 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 types import tempfile import textwrap -from typing import Unpack import unicodedata import unittest import unittest.mock @@ -46,6 +46,7 @@ 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: @@ -4797,6 +4798,26 @@ def test_signature_eval_str(self): par('b', PORK, annotation=tuple), ))) + def test_signature_format_parameter(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, format=Format.SOURCE), + sig([par("x", PORK, annotation="undefined")]) + ) + self.assertEqual( + signature_func(ida.f, format=Format.FORWARDREF), + sig([par("x", PORK, annotation=ForwardRef("undefined"))]) + ) + with self.assertRaisesRegex(NameError, "undefined"): + signature_func(ida.f, 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 From 38c4b427eefe0dad53d7888098e8b0ddc215f282 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 06:21:35 -0700 Subject: [PATCH 2/9] fixes --- Doc/library/inspect.rst | 11 ++++++++++- Doc/whatsnew/3.14.rst | 12 ++++++++++++ Lib/inspect.py | 23 +++++++++++++++++------ Lib/pydoc.py | 5 +---- Lib/test/test_inspect/test_inspect.py | 14 +++++++++++++- Lib/test/test_pydoc/test_pydoc.py | 10 +++++----- 6 files changed, 58 insertions(+), 17 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index af3df61fca6b62..e39a9f3ec45bf6 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -843,7 +843,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, unquote_annotations=False) Create a string representation of the :class:`Signature` object. @@ -852,8 +852,17 @@ function. If the signature is longer than *max_width*, all parameters will be on separate lines. + If *unquote_annotations* is True, :term:`annotations ` + in the signature are displayed without opening and closing quotation + marks. This is useful when the signature was created with the + :attr:`~annotationlib.Format.STRING` format or when + ``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 e60081b08e31ae..fccf1fcc8b6d2e 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -279,6 +279,10 @@ inspect 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 ` are displayed without enclosing quotes. + (Contributed by Jelle Zijlstra in :gh:`101552`.) + json ---- @@ -355,6 +359,14 @@ pickle of the error. (Contributed by Serhiy Storchaka in :gh:`122213`.) +pydoc +----- + +* :term:`Annotations ` 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 fac68a8e845e8d..72a3fb35e9ddba 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1317,7 +1317,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, *, unquote_annotations=False): + if unquote_annotations and isinstance(annotation, str): + return annotation if getattr(annotation, '__module__', None) == 'typing': def repl(match): text = match.group() @@ -2718,13 +2720,17 @@ def replace(self, *, name=_void, kind=_void, return type(self)(name, kind, default=default, annotation=annotation) def __str__(self): + return self._format() + + def _format(self, *, unquote_annotations=False): 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, + unquote_annotations=unquote_annotations) + formatted = '{}: {}'.format(formatted, annotation) if self._default is not _empty: if self._annotation is not _empty: @@ -3193,19 +3199,24 @@ def __repr__(self): def __str__(self): return self.format() - def format(self, *, max_width=None): + def format(self, *, max_width=None, unquote_annotations=False): """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 *unquote_annotations* is True, 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(unquote_annotations=unquote_annotations) kind = param.kind @@ -3242,7 +3253,7 @@ def format(self, *, max_width=None): rendered = '(\n {}\n)'.format(',\n '.join(result)) if self.return_annotation is not _empty: - anno = formatannotation(self.return_annotation) + anno = formatannotation(self.return_annotation, unquote_annotations=unquote_annotations) rendered += ' -> {}'.format(anno) return rendered diff --git a/Lib/pydoc.py b/Lib/pydoc.py index e4b68f796631d5..0ca4699f6705b0 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -218,11 +218,8 @@ def _getargspec(object): name = getattr(object, '__name__', '') # function are always single-line and should not be formatted max_width = (80 - len(name)) if name != '' else None - return signature.format(max_width=max_width) + return signature.format(max_width=max_width, unquote_annotations=True) except (ValueError, TypeError): - import traceback - traceback.print_exc() - raise argspec = getattr(object, '__text_signature__', None) if argspec: if argspec[:2] == '($': diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6d6a6627508a80..070496135b313c 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4566,6 +4566,18 @@ def func( 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(unquote_annotations=True), + "(x: int) -> str" + ) + def test_signature_replace_parameters(self): def test(a, b) -> 42: pass @@ -4806,7 +4818,7 @@ def test_signature_format_parameter(self): for signature_func in (inspect.signature, inspect.Signature.from_callable): with self.subTest(signature_func=signature_func): self.assertEqual( - signature_func(ida.f, format=Format.SOURCE), + signature_func(ida.f, format=Format.STRING), sig([par("x", PORK, annotation="undefined")]) ) self.assertEqual( diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 2dba077cdea6a7..125d52ff8c9be9 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1065,7 +1065,7 @@ def __init__(self, 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 @@ -1074,7 +1074,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 @@ -1101,7 +1101,7 @@ def func( 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__) @@ -1386,8 +1386,8 @@ def foo(data: typing.List[typing.Any], 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)') From 9c7b972fe89f5c949c81c6744a517c18b45dd8d6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 06:38:13 -0700 Subject: [PATCH 3/9] missing file --- Lib/test/test_inspect/inspect_deferred_annotations.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Lib/test/test_inspect/inspect_deferred_annotations.py 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 00000000000000..bb59ef1035b3c1 --- /dev/null +++ b/Lib/test/test_inspect/inspect_deferred_annotations.py @@ -0,0 +1,2 @@ +def f(x: undefined): + pass From b14a719e0b90eb28e42b9ac14ec0ea62d80a2bb5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 06:39:51 -0700 Subject: [PATCH 4/9] blurb --- .../Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst 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 00000000000000..3e3562d8516d63 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst @@ -0,0 +1,4 @@ +Add a *format* parameter to :func:`inspect.signature`. Add an +*unquote_annotations* 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. From 33ef93db6dfbd46326283b810fddb1ce2e2a6ae3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 10:48:15 -0700 Subject: [PATCH 5/9] format -> annotation_format --- Doc/library/inspect.rst | 8 ++++---- Doc/whatsnew/3.14.rst | 2 +- Lib/inspect.py | 18 +++++++++--------- Lib/pydoc.py | 2 +- Lib/test/test_inspect/test_inspect.py | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index e39a9f3ec45bf6..4db9dce8ee1d74 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, format=Format.VALUE) +.. 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*: @@ -722,11 +722,11 @@ function. ``from __future__ import annotations`` was used), :func:`signature` will attempt to automatically un-stringize the annotations using :func:`annotationlib.get_annotations`. The - *globals*, *locals*, *eval_str*, and *format* parameters are passed + *globals*, *locals*, *eval_str*, and *annotation_format* 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 example, use - ``format=annotationlib.Format.STRING`` to return annotations in string + ``annotation_format=annotationlib.Format.STRING`` to return annotations in string format. Raises :exc:`ValueError` if no signature can be provided, and @@ -749,7 +749,7 @@ function. The *globals*, *locals*, and *eval_str* parameters were added. .. versionchanged:: 3.14 - The *format* parameter was added. + The *annotation_format* parameter was added. .. note:: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index fccf1fcc8b6d2e..23c7c6b12d6909 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -275,7 +275,7 @@ module allow the browser to apply its default dark mode. inspect ------- -* :func:`inspect.signature` takes a new argument *format* to control +* :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`.) diff --git a/Lib/inspect.py b/Lib/inspect.py index 72a3fb35e9ddba..a3b0455e8c442d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2271,7 +2271,7 @@ 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, - format=Format.VALUE): + annotation_format=Format.VALUE): """Private helper: constructs Signature for the given python function.""" is_duck_function = False @@ -2298,7 +2298,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, 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, - format=format) + format=annotation_format) defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ @@ -2382,7 +2382,7 @@ def _signature_from_callable(obj, *, locals=None, eval_str=False, sigcls, - format=Format.VALUE): + annotation_format=Format.VALUE): """Private helper function to get signature for arbitrary callable objects. @@ -2395,7 +2395,7 @@ def _signature_from_callable(obj, *, locals=locals, sigcls=sigcls, eval_str=eval_str, - format=format) + annotation_format=annotation_format) if not callable(obj): raise TypeError('{!r} is not a callable object'.format(obj)) @@ -2485,7 +2485,7 @@ def _signature_from_callable(obj, *, return _signature_from_function(sigcls, obj, skip_bound_arg=skip_bound_arg, globals=globals, locals=locals, eval_str=eval_str, - format=format) + annotation_format=annotation_format) if _signature_is_builtin(obj): return _signature_from_builtin(sigcls, obj, @@ -2979,12 +2979,12 @@ def __init__(self, parameters=None, *, return_annotation=_empty, @classmethod def from_callable(cls, obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False, - format=Format.VALUE): + 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, - format=format) + annotation_format=annotation_format) @property def parameters(self): @@ -3260,11 +3260,11 @@ def format(self, *, max_width=None, unquote_annotations=False): def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False, - format=Format.VALUE): + 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, - format=format) + annotation_format=annotation_format) class BufferFlags(enum.IntFlag): diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 0ca4699f6705b0..7a8cb9caa7e163 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -213,7 +213,7 @@ def splitdoc(doc): def _getargspec(object): try: - signature = inspect.signature(object, format=Format.STRING) + signature = inspect.signature(object, annotation_format=Format.STRING) if signature: name = getattr(object, '__name__', '') # function are always single-line and should not be formatted diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 070496135b313c..78b5d0219864a0 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4810,7 +4810,7 @@ def test_signature_eval_str(self): par('b', PORK, annotation=tuple), ))) - def test_signature_format_parameter(self): + def test_signature_annotation_format(self): ida = inspect_deferred_annotations sig = inspect.Signature par = inspect.Parameter @@ -4818,15 +4818,15 @@ def test_signature_format_parameter(self): for signature_func in (inspect.signature, inspect.Signature.from_callable): with self.subTest(signature_func=signature_func): self.assertEqual( - signature_func(ida.f, format=Format.STRING), + signature_func(ida.f, annotation_format=Format.STRING), sig([par("x", PORK, annotation="undefined")]) ) self.assertEqual( - signature_func(ida.f, format=Format.FORWARDREF), + signature_func(ida.f, annotation_format=Format.FORWARDREF), sig([par("x", PORK, annotation=ForwardRef("undefined"))]) ) with self.assertRaisesRegex(NameError, "undefined"): - signature_func(ida.f, format=Format.VALUE) + signature_func(ida.f, annotation_format=Format.VALUE) with self.assertRaisesRegex(NameError, "undefined"): signature_func(ida.f) From afdf4fc01612c1efc0be48afc6d09e2e91fd8f0b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 14:32:16 -0700 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Alex Waygood --- Doc/library/inspect.rst | 4 ++-- Doc/whatsnew/3.14.rst | 2 +- Lib/inspect.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 4db9dce8ee1d74..6b4c35f287f231 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -854,8 +854,8 @@ function. If *unquote_annotations* is True, :term:`annotations ` in the signature are displayed without opening and closing quotation - marks. This is useful when the signature was created with the - :attr:`~annotationlib.Format.STRING` format or when + marks. 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 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 23c7c6b12d6909..be4420be65a1f7 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -280,7 +280,7 @@ inspect (Contributed by Jelle Zijlstra in :gh:`101552`.) * :meth:`inspect.Signature.format` takes a new argument *unquote_annotations*. - If True, string :term:`annotations ` are displayed without enclosing quotes. + If true, string :term:`annotations ` are displayed without surrounding quotes. (Contributed by Jelle Zijlstra in :gh:`101552`.) diff --git a/Lib/inspect.py b/Lib/inspect.py index a3b0455e8c442d..a42ead9e8e0090 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -140,7 +140,8 @@ import abc -from annotationlib import Format, get_annotations # re-exported +from annotationlib import Format +from annotationlib import get_annotations # re-exported import ast import dis import collections.abc @@ -2271,7 +2272,7 @@ 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, - annotation_format=Format.VALUE): + *, annotation_format=Format.VALUE): """Private helper: constructs Signature for the given python function.""" is_duck_function = False @@ -2298,7 +2299,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, 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, - format=annotation_format) + *, format=annotation_format) defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ From 15490802b8492fcac28a29640d4cd238ad4159d7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 14:40:50 -0700 Subject: [PATCH 7/9] Update Lib/inspect.py Co-authored-by: Alex Waygood --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index a42ead9e8e0090..dc0918c4832e01 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2299,7 +2299,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, 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, - *, format=annotation_format) + format=annotation_format) defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ From 35ee105f5ea2df8d70004932bb151df7b330d532 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 30 Sep 2024 21:21:51 -0700 Subject: [PATCH 8/9] Renamings --- Doc/library/inspect.rst | 13 ++++++++----- Lib/inspect.py | 17 +++++++++-------- Lib/pydoc.py | 2 +- Lib/test/test_inspect/test_inspect.py | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 6b4c35f287f231..1eaf1cc5d9a68e 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -722,10 +722,13 @@ function. ``from __future__ import annotations`` was used), :func:`signature` will attempt to automatically un-stringize the annotations using :func:`annotationlib.get_annotations`. The - *globals*, *locals*, *eval_str*, and *annotation_format* parameters are passed + *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 example, use + 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. @@ -843,7 +846,7 @@ function. :class:`Signature` objects are also supported by the generic function :func:`copy.replace`. - .. method:: format(*, max_width=None, unquote_annotations=False) + .. method:: format(*, max_width=None, quote_annotation_strings=True) Create a string representation of the :class:`Signature` object. @@ -852,9 +855,9 @@ function. If the signature is longer than *max_width*, all parameters will be on separate lines. - If *unquote_annotations* is True, :term:`annotations ` + If *quote_annotation_strings* is False, :term:`annotations ` in the signature are displayed without opening and closing quotation - marks. This is useful if the signature was created with the + 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. diff --git a/Lib/inspect.py b/Lib/inspect.py index f4fbb566e8f3f7..c51ea30995e08f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1320,8 +1320,8 @@ def getargvalues(frame): args, varargs, varkw = getargs(frame.f_code) return ArgInfo(args, varargs, varkw, frame.f_locals) -def formatannotation(annotation, base_module=None, *, unquote_annotations=False): - if unquote_annotations and isinstance(annotation, str): +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): @@ -2725,14 +2725,14 @@ def replace(self, *, name=_void, kind=_void, def __str__(self): return self._format() - def _format(self, *, unquote_annotations=False): + def _format(self, *, quote_annotation_strings=True): kind = self.kind formatted = self._name # Add annotation and default value if self._annotation is not _empty: annotation = formatannotation(self._annotation, - unquote_annotations=unquote_annotations) + quote_annotation_strings=quote_annotation_strings) formatted = '{}: {}'.format(formatted, annotation) if self._default is not _empty: @@ -3202,7 +3202,7 @@ def __repr__(self): def __str__(self): return self.format() - def format(self, *, max_width=None, unquote_annotations=False): + def format(self, *, max_width=None, quote_annotation_strings=True): """Create a string representation of the Signature object. If *max_width* integer is passed, @@ -3210,7 +3210,7 @@ def format(self, *, max_width=None, unquote_annotations=False): If signature is longer than *max_width*, all parameters will be on separate lines. - If *unquote_annotations* is True, annotations + 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. @@ -3219,7 +3219,7 @@ def format(self, *, max_width=None, unquote_annotations=False): render_pos_only_separator = False render_kw_only_separator = True for param in self.parameters.values(): - formatted = param._format(unquote_annotations=unquote_annotations) + formatted = param._format(quote_annotation_strings=quote_annotation_strings) kind = param.kind @@ -3256,7 +3256,8 @@ def format(self, *, max_width=None, unquote_annotations=False): rendered = '(\n {}\n)'.format(',\n '.join(result)) if self.return_annotation is not _empty: - anno = formatannotation(self.return_annotation, unquote_annotations=unquote_annotations) + anno = formatannotation(self.return_annotation, + quote_annotation_strings=quote_annotation_strings) rendered += ' -> {}'.format(anno) return rendered diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d8d7a36d655d88..c863794ea14ef9 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -218,7 +218,7 @@ def _getargspec(object): name = getattr(object, '__name__', '') # function are always single-line and should not be formatted max_width = (80 - len(name)) if name != '' else None - return signature.format(max_width=max_width, unquote_annotations=True) + 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/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 4edc3e3a5ffee9..28f480ce11d02a 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4631,7 +4631,7 @@ def func(x: 'int') -> 'str': ... "(x: 'int') -> 'str'" ) self.assertEqual( - inspect.signature(func).format(unquote_annotations=True), + inspect.signature(func).format(quote_annotation_strings=False), "(x: int) -> str" ) From c549c705103e8d0ca268bc6d54e6a2e447365058 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 8 Oct 2024 21:38:38 -0700 Subject: [PATCH 9/9] Update Misc/NEWS.d/next/Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst --- .../Library/2024-09-27-06-39-32.gh-issue-101552.xYkzag.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3e3562d8516d63..913a84de5fe6a3 100644 --- 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 @@ -1,4 +1,4 @@ -Add a *format* parameter to :func:`inspect.signature`. Add an -*unquote_annotations* parameter to :meth:`inspect.Signature.format`. Use the +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.