From 088a5cce58de9a3468d822cbac695f8fc0678938 Mon Sep 17 00:00:00 2001 From: eamanu Date: Mon, 22 Apr 2019 17:17:33 -0300 Subject: [PATCH 1/9] Add follow_wrapped to help builtin When print the docstring using help builtin for an wrapper function it doesn't correspond to the function wrapped. For avoid this this PR add the follow_wrapped to help(). --- Lib/pydoc.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 628f9fc7d1d1ef..d9fd6080f0ed61 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1772,9 +1772,11 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0, return title % desc + '\n\n' + renderer.document(object, name) def doc(thing, title='Python Library Documentation: %s', forceload=0, - output=None): + output=None, follow_wrapped=False): """Display text documentation, given an object or a path to an object.""" try: + if follow_wrapped: + thing = inspect.unwrap(thing) if output is None: pager(render_doc(thing, title, forceload)) else: @@ -1995,9 +1997,9 @@ def __repr__(self): self.__class__.__qualname__) _GoInteractive = object() - def __call__(self, request=_GoInteractive): + def __call__(self, request=_GoInteractive, follow_wrapped=False): if request is not self._GoInteractive: - self.help(request) + self.help(request, follow_wrapped=follow_wrapped) else: self.intro() self.interact() @@ -2038,7 +2040,7 @@ def getline(self, prompt): self.output.flush() return self.input.readline() - def help(self, request): + def help(self, request, follow_wrapped=False): if type(request) is type(''): request = request.strip() if request == 'keywords': self.listkeywords() @@ -2050,13 +2052,16 @@ def help(self, request): elif request in self.symbols: self.showsymbol(request) elif request in ['True', 'False', 'None']: # special case these keywords since they are objects too - doc(eval(request), 'Help on %s:') + doc(eval(request), 'Help on %s:', follow_wrapped=follow_wrapped) elif request in self.keywords: self.showtopic(request) elif request in self.topics: self.showtopic(request) - elif request: doc(request, 'Help on %s:', output=self._output) - else: doc(str, 'Help on %s:', output=self._output) + elif request: doc(request, 'Help on %s:', output=self._output, + follow_wrapped=follow_wrapped) + else: doc(str, 'Help on %s:', output=self._output, + follow_wrapped=follow_wrapped) elif isinstance(request, Helper): self() - else: doc(request, 'Help on %s:', output=self._output) + else: doc(request, 'Help on %s:', output=self._output, + follow_wrapped=follow_wrapped) self.output.write('\n') def intro(self): From 0af8ea857cc63b57b765dba5478a639621c68c67 Mon Sep 17 00:00:00 2001 From: eamanu Date: Mon, 22 Apr 2019 17:25:55 -0300 Subject: [PATCH 2/9] Add NEW --- .../next/Library/2019-04-22-17-25-46.bpo-29940.fslqG1.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-04-22-17-25-46.bpo-29940.fslqG1.rst diff --git a/Misc/NEWS.d/next/Library/2019-04-22-17-25-46.bpo-29940.fslqG1.rst b/Misc/NEWS.d/next/Library/2019-04-22-17-25-46.bpo-29940.fslqG1.rst new file mode 100644 index 00000000000000..61a1a878f3bfbb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-22-17-25-46.bpo-29940.fslqG1.rst @@ -0,0 +1,2 @@ +Add follow_wrapped to help function to print the docstring of a wrapped +function. From 45dc1af6bc5e4bbee96d00f64cf49cc03d55971e Mon Sep 17 00:00:00 2001 From: eamanu Date: Mon, 22 Apr 2019 17:48:08 -0300 Subject: [PATCH 3/9] Fix whitespace on Lib/pydoc.py --- Lib/pydoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d9fd6080f0ed61..3640c938960987 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -2055,7 +2055,7 @@ def help(self, request, follow_wrapped=False): doc(eval(request), 'Help on %s:', follow_wrapped=follow_wrapped) elif request in self.keywords: self.showtopic(request) elif request in self.topics: self.showtopic(request) - elif request: doc(request, 'Help on %s:', output=self._output, + elif request: doc(request, 'Help on %s:', output=self._output, follow_wrapped=follow_wrapped) else: doc(str, 'Help on %s:', output=self._output, follow_wrapped=follow_wrapped) From 8f089f1547c8e07204a31ce312a2f5ec6ba04fd1 Mon Sep 17 00:00:00 2001 From: eamanu Date: Thu, 25 Apr 2019 20:49:14 -0300 Subject: [PATCH 4/9] Change follow_wrapped parameter to be True for default --- Lib/pydoc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 3640c938960987..de7ad259c22da4 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1772,10 +1772,10 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0, return title % desc + '\n\n' + renderer.document(object, name) def doc(thing, title='Python Library Documentation: %s', forceload=0, - output=None, follow_wrapped=False): + output=None, follow_wrapped=True): """Display text documentation, given an object or a path to an object.""" try: - if follow_wrapped: + if not follow_wrapped: thing = inspect.unwrap(thing) if output is None: pager(render_doc(thing, title, forceload)) @@ -2040,7 +2040,7 @@ def getline(self, prompt): self.output.flush() return self.input.readline() - def help(self, request, follow_wrapped=False): + def help(self, request, follow_wrapped=True): if type(request) is type(''): request = request.strip() if request == 'keywords': self.listkeywords() From 2832e99f38b413a2ed2216a3083376b3570d6c5d Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Mon, 21 Sep 2020 18:20:00 -0300 Subject: [PATCH 5/9] Update Lib/pydoc.py Co-authored-by: Tal Einat --- Lib/pydoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index de7ad259c22da4..33d20e6db25f17 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1776,7 +1776,7 @@ def doc(thing, title='Python Library Documentation: %s', forceload=0, """Display text documentation, given an object or a path to an object.""" try: if not follow_wrapped: - thing = inspect.unwrap(thing) + thing = inspect.unwrap(thing, stop=lambda func: getattr(func, '__doc__', None) if output is None: pager(render_doc(thing, title, forceload)) else: From e44927c1707442e89541b51fd08e39251f95f89d Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Mon, 21 Sep 2020 20:38:25 -0300 Subject: [PATCH 6/9] Fix pydoc to show successfully the documentation when the wrapped func has doc. Otherwise show parents __doc__ --- Lib/pydoc.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 33d20e6db25f17..a1eafa3de76c57 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1775,13 +1775,14 @@ def doc(thing, title='Python Library Documentation: %s', forceload=0, output=None, follow_wrapped=True): """Display text documentation, given an object or a path to an object.""" try: - if not follow_wrapped: - thing = inspect.unwrap(thing, stop=lambda func: getattr(func, '__doc__', None) + if follow_wrapped: + thing = inspect.unwrap(thing, + stop=(lambda func: not hasattr(func, '__doc__'))) if output is None: pager(render_doc(thing, title, forceload)) else: output.write(render_doc(thing, title, forceload, plaintext)) - except (ImportError, ErrorDuringImport) as value: + except (ImportError, ErrorDuringImport, ValueError) as value: print(value) def writedoc(thing, forceload=0): @@ -1997,7 +1998,7 @@ def __repr__(self): self.__class__.__qualname__) _GoInteractive = object() - def __call__(self, request=_GoInteractive, follow_wrapped=False): + def __call__(self, request=_GoInteractive, follow_wrapped=True): if request is not self._GoInteractive: self.help(request, follow_wrapped=follow_wrapped) else: From 997c8bdf7db316c2078419f79f311ad18b996b10 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Mon, 21 Sep 2020 21:02:35 -0300 Subject: [PATCH 7/9] Show help when __docs__ is available --- Lib/pydoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index a1eafa3de76c57..422a4c984207ad 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1777,7 +1777,7 @@ def doc(thing, title='Python Library Documentation: %s', forceload=0, try: if follow_wrapped: thing = inspect.unwrap(thing, - stop=(lambda func: not hasattr(func, '__doc__'))) + stop=(lambda func: not getattr(func, '__doc__', True))) if output is None: pager(render_doc(thing, title, forceload)) else: From f147a93f7b5d2b659f60c134320edce6985c6a8e Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Mon, 21 Sep 2020 21:50:18 -0300 Subject: [PATCH 8/9] add tests --- Lib/test/test_pydoc.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 76d2af8e461ed1..75f7b46517d38c 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -647,6 +647,41 @@ def test_builtin_on_metaclasses(self): # Testing that the subclasses section does not appear self.assertNotIn('Built-in subclasses', text) + def test_builtin_with_wrapper(self): + """Test help on function wrapped. + + When running help() on a function that is wrapped, + should be show the docstring of the last object in the chain + with __doc__ method. + """ + + import functools + class TestWrapper: + """This is the docstring of Wrapper""" + def __init__(self, func): + functools.update_wrapper(func) + def __call__(self): + pass + + @TestWrapper + def test_func1(): + """Test func1""" + pass + + doc = pydoc.TextDoc() + text = doc.docclass(test_func1) + self.assertIn('Test func1', text) + + text = doc.docclass(test_func1, follow_wrapped=False) + self.assertIn("This is the docstring for Wrapper", text) + + @TestWrapper + def test_func2(): + pass + + text = doc.docclass(test_func1) + self.assertIn("This is the docstring for wrapper", text) + @unittest.skipIf(sys.flags.optimize >= 2, 'Docstrings are omitted with -O2 and above') @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(), From 54603eeffdb421a231c329119de22b411e8506d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Wed, 23 Sep 2020 17:23:34 -0300 Subject: [PATCH 9/9] Fix code and test --- Lib/pydoc.py | 5 ++--- Lib/test/test_pydoc.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 422a4c984207ad..9e38a200d62daa 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1775,9 +1775,8 @@ def doc(thing, title='Python Library Documentation: %s', forceload=0, output=None, follow_wrapped=True): """Display text documentation, given an object or a path to an object.""" try: - if follow_wrapped: - thing = inspect.unwrap(thing, - stop=(lambda func: not getattr(func, '__doc__', True))) + if not follow_wrapped: + thing = type(thing) if output is None: pager(render_doc(thing, title, forceload)) else: diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 75f7b46517d38c..1d2e1ebc1e4399 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -647,7 +647,7 @@ def test_builtin_on_metaclasses(self): # Testing that the subclasses section does not appear self.assertNotIn('Built-in subclasses', text) - def test_builtin_with_wrapper(self): + def test_help_with_wrapper(self): """Test help on function wrapped. When running help() on a function that is wrapped, @@ -659,7 +659,7 @@ def test_builtin_with_wrapper(self): class TestWrapper: """This is the docstring of Wrapper""" def __init__(self, func): - functools.update_wrapper(func) + functools.update_wrapper(self, func) def __call__(self): pass @@ -668,19 +668,22 @@ def test_func1(): """Test func1""" pass - doc = pydoc.TextDoc() - text = doc.docclass(test_func1) - self.assertIn('Test func1', text) + buff = StringIO() + + helper = pydoc.Helper(output=buff) + + helper.help(test_func1) + self.assertIn('Test func1', buff.getvalue().strip()) - text = doc.docclass(test_func1, follow_wrapped=False) - self.assertIn("This is the docstring for Wrapper", text) + helper.help(test_func1, follow_wrapped=False) + self.assertIn("This is the docstring of Wrapper", buff.getvalue().strip()) @TestWrapper def test_func2(): pass - text = doc.docclass(test_func1) - self.assertIn("This is the docstring for wrapper", text) + helper.help(test_func2) + self.assertIn("This is the docstring of Wrapper", buff.getvalue().strip()) @unittest.skipIf(sys.flags.optimize >= 2, 'Docstrings are omitted with -O2 and above')