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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy