]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-28249: fix `lineno` location for empty `DocTest` instances (GH-30498) (GH-92978)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 19 May 2022 18:03:06 +0000 (11:03 -0700)
committerGitHub <noreply@github.com>
Thu, 19 May 2022 18:03:06 +0000 (20:03 +0200)
(cherry picked from commit 8db2b3b6878aba9f12844526bce966b7eed81aee)

Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
Lib/doctest.py
Lib/test/doctest_lineno.py [new file with mode: 0644]
Lib/test/test_doctest.py
Misc/NEWS.d/next/Library/2022-01-09-14-23-00.bpo-28249.4dzB80.rst [new file with mode: 0644]

index ed94d15c0e2da4812954f621b616b9b62b301659..b2ef2ce63672ebdc4ed83001f421bbc3711acbf5 100644 (file)
@@ -1085,19 +1085,21 @@ class DocTestFinder:
 
     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.
+
+        Returns `None` if the given 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 @@ class DocTestFinder:
 
         # 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 (file)
index 0000000..be19851
--- /dev/null
@@ -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'
+        """
index 3e7f3782d89f4233e66ee103825433dd6059c8c4..a4aab6cf4db3b5dae55951568683e8d744c0824b 100644 (file)
@@ -25,6 +25,7 @@ if not support.has_subprocess_support:
 
 # 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 @@ We'll simulate a __file__ attr that ends in pyc:
     >>> tests = finder.find(sample_func)
 
     >>> print(tests)  # doctest: +ELLIPSIS
-    [<DocTest sample_func from test_doctest.py:33 (1 example)>]
+    [<DocTest sample_func from test_doctest.py:34 (1 example)>]
 
 The exact name depends on how test_doctest was invoked, so allow for
 leading path components.
@@ -642,6 +643,26 @@ displays.
      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 (file)
index 0000000..b5f1312
--- /dev/null
@@ -0,0 +1,2 @@
+Set :attr:`doctest.DocTest.lineno` to ``None`` when object does not have
+:attr:`__doc__`.