From d0d69c255fb41aadee0370833731dbc2af24a6ff Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Fri, 28 Jul 2023 22:31:33 +0300 Subject: [PATCH 01/10] gh-107155: Fix `help(lambda_func)` when `lambda_func` has `__annotations__` with `return` key. --- Lib/pydoc.py | 7 +++---- Lib/test/test_pydoc.py | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 185f09e603df2e..22a7527e9237e3 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1500,10 +1500,9 @@ def docroutine(self, object, name=None, mod=None, cl=None): argspec = str(signature) if realname == '': title = self.bold(name) + ' lambda ' - # XXX lambda's won't usually have func_annotations['return'] - # since the syntax doesn't support but it is possible. - # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses + # Since lambda's cannot have a parenthesis in their signature, + # it's safe to replace them. + argspec = argspec.replace("(", "").replace(")", "") if not argspec: argspec = '(...)' decl = asyncqualifier + title + argspec + note diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index ddb5187f90da9b..8dc9a13e021c7d 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -684,6 +684,14 @@ def test_help_output_redirect(self): finally: pydoc.getpager = getpager_old + def test_lambda_with_annotations(self): + func = lambda a, b, c: 1 + func.__annotations__ = {"return": int} + with captured_stdout() as help_io: + pydoc.help(func) + helptext = help_io.getvalue() + self.assertIn("lambda a, b, c -> int", helptext) + def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) with captured_stdout() as help_io: From 5a2133b2eb48324db1bb02a2ed27a7d8b066d735 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:49:47 +0100 Subject: [PATCH 02/10] Manipulate lambda annotations before converting the signature to a string --- Lib/pydoc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 22a7527e9237e3..9bba7a43233b51 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1492,6 +1492,10 @@ def docroutine(self, object, name=None, mod=None, cl=None): argspec = None if inspect.isroutine(object): + if realname == '': + retann = object.__annotations__.pop('return', None) + else: + retann = None try: signature = inspect.signature(object) except (ValueError, TypeError): @@ -1500,9 +1504,10 @@ def docroutine(self, object, name=None, mod=None, cl=None): argspec = str(signature) if realname == '': title = self.bold(name) + ' lambda ' - # Since lambda's cannot have a parenthesis in their signature, - # it's safe to replace them. - argspec = argspec.replace("(", "").replace(")", "") + argspec = argspec[1:-1] # remove parentheses + # add a return annotation, should one exist + if retann is not None: + argspec += f' -> {inspect.formatannotation(retann)}' if not argspec: argspec = '(...)' decl = asyncqualifier + title + argspec + note From 7802354fed262f97f350693121511afbf75f0e50 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:02:18 +0100 Subject: [PATCH 03/10] Add a simple test --- Lib/test/test_pydoc.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 8dc9a13e021c7d..d104add503800c 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -684,7 +684,7 @@ def test_help_output_redirect(self): finally: pydoc.getpager = getpager_old - def test_lambda_with_annotations(self): + def test_lambda_return_annotation(self): func = lambda a, b, c: 1 func.__annotations__ = {"return": int} with captured_stdout() as help_io: @@ -692,6 +692,16 @@ def test_lambda_with_annotations(self): helptext = help_io.getvalue() self.assertIn("lambda a, b, c -> int", helptext) + def test_lambda_return_annotation_tuple(self): + # test of a tuple return annotation, + # for a potential PEP 677-like future use + func = lambda a, b, c: (1, "") + func.__annotations__ = {"return": (int, str)} + with captured_stdout() as help_io: + pydoc.help(func) + helptext = help_io.getvalue() + self.assertIn("lambda a, b, c -> (, )", helptext) + def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) with captured_stdout() as help_io: From 0deb7ac2761e59b6a423fc740823ccde72de9263 Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Wed, 2 Aug 2023 01:26:38 +0300 Subject: [PATCH 04/10] Add NEWS entry --- .../next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst diff --git a/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst new file mode 100644 index 00000000000000..00a647f453f98a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst @@ -0,0 +1,2 @@ +Fix incorrect output of ``help(x)`` where ``x`` is :keyword:`lambda` function, +which has ``__annotations__`` attribute with ``"return"`` key. \ No newline at end of file From 6f5b10aae62bd97512ec3751f7506e31238ead6c Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Wed, 2 Aug 2023 01:43:40 +0300 Subject: [PATCH 05/10] Update Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst Co-authored-by: Alex Waygood --- .../Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst index 00a647f453f98a..301a8e0941d6a5 100644 --- a/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst +++ b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst @@ -1,2 +1,3 @@ -Fix incorrect output of ``help(x)`` where ``x`` is :keyword:`lambda` function, -which has ``__annotations__`` attribute with ``"return"`` key. \ No newline at end of file +Fix incorrect output of ``help(x)`` where ``x`` is a :keyword:`lambda` +function, which has an ``__annotations__`` dictionary attribute with a +``"return"`` key. \ No newline at end of file From 52f6c5cef744aa534c1f48f572ca49e5c303244d Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Mon, 1 Jan 2024 21:07:16 +0300 Subject: [PATCH 06/10] Add a newline --- .../next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst index 301a8e0941d6a5..8362dc0fcfaa74 100644 --- a/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst +++ b/Misc/NEWS.d/next/Library/2023-08-02-01-17-32.gh-issue-107155.Mj1K9L.rst @@ -1,3 +1,3 @@ Fix incorrect output of ``help(x)`` where ``x`` is a :keyword:`lambda` function, which has an ``__annotations__`` dictionary attribute with a -``"return"`` key. \ No newline at end of file +``"return"`` key. From e5cf953ce784d328b3663d8304ec9fd7b1ef5b33 Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Sat, 17 Feb 2024 13:46:55 +0200 Subject: [PATCH 07/10] Remove parenthesis only if object doesn't have annotations --- Lib/pydoc.py | 16 +++++++--------- Lib/test/test_pydoc.py | 18 ------------------ 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 1f9272724d7268..6dfa0c7fdafc74 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1087,7 +1087,8 @@ def docroutine(self, object, name=None, mod=None, # XXX lambda's won't usually have func_annotations['return'] # since the syntax doesn't support but it is possible. # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses + if not object.__annotations__: + argspec = argspec[1:-1] # remove parentheses if not argspec: argspec = '(...)' @@ -1500,17 +1501,14 @@ def docroutine(self, object, name=None, mod=None, cl=None): argspec = None if inspect.isroutine(object): - if realname == "": - retann = object.__annotations__.pop("return", None) - else: - retann = None argspec = _getargspec(object) if argspec and realname == '': title = self.bold(name) + ' lambda ' - argspec = argspec[1:-1] - if retann is not None: - argspec += f' -> {inspect.formatannotation(retann)}' - object.__annotations__['return'] = retann + # XXX lambda's won't usually have func_annotations['return'] + # since the syntax doesn't support but it is possible. + # So removing parentheses isn't truly safe. + if not object.__annotations__: + argspec = argspec[1:-1] if not argspec: argspec = '(...)' decl = asyncqualifier + title + argspec + note diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 572c09c500aec3..99b19d01783a10 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -687,24 +687,6 @@ def test_help_output_redirect(self): finally: pydoc.getpager = getpager_old - def test_lambda_return_annotation(self): - func = lambda a, b, c: 1 - func.__annotations__ = {"return": int} - with captured_stdout() as help_io: - pydoc.help(func) - helptext = help_io.getvalue() - self.assertIn("lambda a, b, c -> int", helptext) - - def test_lambda_return_annotation_tuple(self): - # test of a tuple return annotation, - # for a potential PEP 677-like future use - func = lambda a, b, c: (1, "") - func.__annotations__ = {"return": (int, str)} - with captured_stdout() as help_io: - pydoc.help(func) - helptext = help_io.getvalue() - self.assertIn("lambda a, b, c -> (, )", helptext) - def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) with captured_stdout() as help_io: From 4d0e5f693451b9ac89cceeaefb5f86419688ea53 Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Sat, 17 Feb 2024 13:52:21 +0200 Subject: [PATCH 08/10] Add test case --- Lib/test/test_pydoc/test_pydoc.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 0dd24e6d347364..04ea1351719566 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -693,6 +693,14 @@ def test_help_output_redirect(self): finally: pydoc.getpager = getpager_old + def test_lambda_return_annotation(self): + func = lambda a, b, c: 1 + func.__annotations__ = {"return": int} + with captured_output('stdout') as help_io: + pydoc.help(func) + helptext = help_io.getvalue() + self.assertIn("lambda (a, b, c) -> 'int'", helptext) + def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) with captured_stdout() as help_io: From c0ce39472ccf3f69367372d4a3b9ff231b68f69d Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Sat, 17 Feb 2024 14:02:31 +0200 Subject: [PATCH 09/10] Fix test --- Lib/test/test_pydoc/test_pydoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 04ea1351719566..b384566fb3e347 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -699,7 +699,7 @@ def test_lambda_return_annotation(self): with captured_output('stdout') as help_io: pydoc.help(func) helptext = help_io.getvalue() - self.assertIn("lambda (a, b, c) -> 'int'", helptext) + self.assertIn("lambda (a, b, c) -> int", helptext) def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) From 63fe69293d485af3282bce258cfd2324bc9738ec Mon Sep 17 00:00:00 2001 From: Kirill Podoprigora Date: Sat, 17 Feb 2024 14:22:25 +0200 Subject: [PATCH 10/10] Add more test cases --- Lib/test/test_pydoc/test_pydoc.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index b384566fb3e347..d7a333a1103eac 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -693,7 +693,7 @@ def test_help_output_redirect(self): finally: pydoc.getpager = getpager_old - def test_lambda_return_annotation(self): + def test_lambda_with_return_annotation(self): func = lambda a, b, c: 1 func.__annotations__ = {"return": int} with captured_output('stdout') as help_io: @@ -701,6 +701,22 @@ def test_lambda_return_annotation(self): helptext = help_io.getvalue() self.assertIn("lambda (a, b, c) -> int", helptext) + def test_lambda_without_return_annotation(self): + func = lambda a, b, c: 1 + func.__annotations__ = {"a": int, "b": int, "c": int} + with captured_output('stdout') as help_io: + pydoc.help(func) + helptext = help_io.getvalue() + self.assertIn("lambda (a: int, b: int, c: int)", helptext) + + def test_lambda_with_return_and_params_annotation(self): + func = lambda a, b, c: 1 + func.__annotations__ = {"a": int, "b": int, "c": int, "return": int} + with captured_output('stdout') as help_io: + pydoc.help(func) + helptext = help_io.getvalue() + self.assertIn("lambda (a: int, b: int, c: int) -> int", helptext) + def test_namedtuple_fields(self): Person = namedtuple('Person', ['nickname', 'firstname']) with captured_stdout() as help_io: