From d9e87bf6decc558852bab47756cb965fd31be199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 19 May 2022 17:11:34 +0200 Subject: [PATCH 1/4] bpo-28249: fix `lineno` location for empty `DocTest` instances --- Lib/doctest.py | 14 ++++-- Lib/test/doctest_lineno.py | 50 +++++++++++++++++++ Lib/test/test_doctest.py | 23 ++++++++- .../2022-01-09-14-23-00.bpo-28249.4dzB80.rst | 2 + 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 Lib/test/doctest_lineno.py create mode 100644 Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst diff --git a/Lib/doctest.py b/Lib/doctest.py index ed94d15c0e2da4..b66fa2e2230eaa 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1085,19 +1085,21 @@ def _get_test(self, obj, name, module, globs, source_lines): def _find_lineno(self, obj, source_lines): """ - Return a line number of the given object's docstring. Note: - this method assumes that the object has a docstring. + Return a line number of the given object's docstring. + + This method returns `None` if an object does not have a docstring. """ lineno = None + docstring = getattr(obj, '__doc__', None) # Find the line number for modules. - if inspect.ismodule(obj): + if inspect.ismodule(obj) and docstring is not None: lineno = 0 # Find the line number for classes. # Note: this could be fooled if a class is defined multiple # times in a single file. - if inspect.isclass(obj): + if inspect.isclass(obj) and docstring is not None: if source_lines is None: return None pat = re.compile(r'^\s*class\s*%s\b' % @@ -1109,7 +1111,9 @@ def _find_lineno(self, obj, source_lines): # Find the line number for functions & methods. if inspect.ismethod(obj): obj = obj.__func__ - if inspect.isfunction(obj): obj = obj.__code__ + if inspect.isfunction(obj) and getattr(obj, '__doc__', None): + # We don't use `docstring` var here, because `obj` can be changed. + obj = obj.__code__ if inspect.istraceback(obj): obj = obj.tb_frame if inspect.isframe(obj): obj = obj.f_code if inspect.iscode(obj): diff --git a/Lib/test/doctest_lineno.py b/Lib/test/doctest_lineno.py new file mode 100644 index 00000000000000..be198513a1502c --- /dev/null +++ b/Lib/test/doctest_lineno.py @@ -0,0 +1,50 @@ +# This module is used in `test_doctest`. +# It must not have a docstring. + +def func_with_docstring(): + """Some unrelated info.""" + + +def func_without_docstring(): + pass + + +def func_with_doctest(): + """ + This function really contains a test case. + + >>> func_with_doctest.__name__ + 'func_with_doctest' + """ + return 3 + + +class ClassWithDocstring: + """Some unrelated class information.""" + + +class ClassWithoutDocstring: + pass + + +class ClassWithDoctest: + """This class really has a test case in it. + + >>> ClassWithDoctest.__name__ + 'ClassWithDoctest' + """ + + +class MethodWrapper: + def method_with_docstring(self): + """Method with a docstring.""" + + def method_without_docstring(self): + pass + + def method_with_doctest(self): + """ + This has a doctest! + >>> MethodWrapper.method_with_doctest.__name__ + 'method_with_doctest' + """ diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 3e7f3782d89f42..a4aab6cf4db3b5 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -25,6 +25,7 @@ # NOTE: There are some additional tests relating to interaction with # zipimport in the test_zipimport_support test module. +# There are also related tests in `test_doctest2` module. ###################################################################### ## Sample Objects (used by test cases) @@ -460,7 +461,7 @@ def basics(): r""" >>> tests = finder.find(sample_func) >>> print(tests) # doctest: +ELLIPSIS - [] + [] The exact name depends on how test_doctest was invoked, so allow for leading path components. @@ -642,6 +643,26 @@ def basics(): r""" 1 SampleClass.double 1 SampleClass.get +When used with `exclude_empty=False` we are also interested in line numbers +of doctests that are empty. +It used to be broken for quite some time until `bpo-28249`. + + >>> from test import doctest_lineno + >>> tests = doctest.DocTestFinder(exclude_empty=False).find(doctest_lineno) + >>> for t in tests: + ... print('%5s %s' % (t.lineno, t.name)) + None test.doctest_lineno + 22 test.doctest_lineno.ClassWithDocstring + 30 test.doctest_lineno.ClassWithDoctest + None test.doctest_lineno.ClassWithoutDocstring + None test.doctest_lineno.MethodWrapper + 39 test.doctest_lineno.MethodWrapper.method_with_docstring + 45 test.doctest_lineno.MethodWrapper.method_with_doctest + None test.doctest_lineno.MethodWrapper.method_without_docstring + 4 test.doctest_lineno.func_with_docstring + 12 test.doctest_lineno.func_with_doctest + None test.doctest_lineno.func_without_docstring + Turning off Recursion ~~~~~~~~~~~~~~~~~~~~~ DocTestFinder can be told not to look for tests in contained objects diff --git a/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst new file mode 100644 index 00000000000000..aeb46bca08468e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst @@ -0,0 +1,2 @@ +Set :attr:`doctest.DocTest.lineno` to `None` when object does not have +``__doc__``. From 44e607546f946e2ffdc0dc1b515e273c7078e8d5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 9 Jan 2022 14:46:34 +0300 Subject: [PATCH 2/4] Better wording --- Lib/doctest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index b66fa2e2230eaa..b2ef2ce63672eb 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1087,7 +1087,7 @@ def _find_lineno(self, obj, source_lines): """ Return a line number of the given object's docstring. - This method returns `None` if an object does not have a docstring. + Returns `None` if the given object does not have a docstring. """ lineno = None docstring = getattr(obj, '__doc__', None) From f56e16ab83d060b1eb11085e5b849f295274c63e Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 9 Jan 2022 14:47:32 +0300 Subject: [PATCH 3/4] Update 2022-01-09-14-23-00.bpo-28249.4dzB80.rst --- .../next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst index aeb46bca08468e..d06e9f51f03206 100644 --- a/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst +++ b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst @@ -1,2 +1,2 @@ -Set :attr:`doctest.DocTest.lineno` to `None` when object does not have +Set :attr:`doctest.DocTest.lineno` to ``None`` when object does not have ``__doc__``. From d893c7c5cab9cb4c4aeadfb08c01f1d9acbe9f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 19 May 2022 17:07:16 +0200 Subject: [PATCH 4/4] Update Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> --- .../next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst index d06e9f51f03206..b5f1312d768669 100644 --- a/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst +++ b/Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst @@ -1,2 +1,2 @@ Set :attr:`doctest.DocTest.lineno` to ``None`` when object does not have -``__doc__``. +:attr:`__doc__`.