Skip to content

Commit

Permalink
include all modules in doctests
Browse files Browse the repository at this point in the history
This patch changes doctest's test finder to include all files in the nutils
directory, rather than the ones listed in nutils.__all__ which is due to be
removed. The change includes a modified version of DocTestFinder to fix a bug
triggered by the SI module (formerly untested) and for which a fix is pending
for cpython (python/cpython#107716). The patch also
includes a small fix in SI's documentation.
  • Loading branch information
gertjanvanzwieten committed Aug 7, 2023
1 parent 8b69a53 commit 0d5a714
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 3 deletions.
1 change: 1 addition & 0 deletions nutils/SI.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
prefixes. Dimensional values are generated primarily by instantiating the
Quantity type with a string value.
>>> from nutils import SI
>>> v = SI.parse('7μN*5h/6g')
The Quantity constructor recognizes the multiplication (\*) and division (/)
Expand Down
64 changes: 64 additions & 0 deletions nutils/_backports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,67 @@
implementations found in this module should not be relied upon as general
drop-on replacements.
"""


if False: # awaiting introduction

from doctest import DocTestFinder

else:

# This is a modified version of doctest.DocTestFinder to fix issue
# https://github.com/python/cpython/issues/107715, which prevents doctest
# operation for the SI module. The modification assumes that `find` relies
# on the internal `_find_lineno` method.

import doctest, inspect, re

class DocTestFinder(doctest.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.
"""
lineno = None

# Find the line number for modules.
if inspect.ismodule(obj):
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 source_lines is None:
return None
pat = re.compile(r'^\s*class\s*%s\b' %
re.escape(getattr(obj, '__name__', '-')))
for i, line in enumerate(source_lines):
if pat.match(line):
lineno = i
break

# Find the line number for functions & methods.
if inspect.ismethod(obj): obj = obj.__func__
if inspect.isfunction(obj): obj = obj.__code__
if inspect.istraceback(obj): obj = obj.tb_frame
if inspect.isframe(obj): obj = obj.f_code
if inspect.iscode(obj):
lineno = getattr(obj, 'co_firstlineno', None)-1

# Find the line number where the docstring starts. Assume
# that it's the first line that begins with a quote mark.
# Note: this could be fooled by a multiline function
# signature, where a continuation line begins with a quote
# mark.
if lineno is not None:
if source_lines is None:
return lineno+1
pat = re.compile(r'(^|.*:)\s*\w*("|\')')
for lineno in range(lineno, len(source_lines)):
if pat.match(source_lines[lineno]):
return lineno

# We couldn't find the line number.
return None
7 changes: 4 additions & 3 deletions tests/test_docs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import doctest as _doctest
from nutils._backports import DocTestFinder
import unittest
import importlib
import os
Expand Down Expand Up @@ -59,11 +60,11 @@ def __repr__(self):

doctest = unittest.TestSuite()
parser = _doctest.DocTestParser()
finder = _doctest.DocTestFinder(parser=parser)
finder = DocTestFinder(parser=parser)
checker = nutils.testing.FloatNeighborhoodOutputChecker()
root = pathlib.Path(__file__).parent.parent
for name in nutils.__all__:
module = importlib.import_module('.'+name, 'nutils')
for path in sorted((root/'nutils').glob('*.py')):
module = importlib.import_module('.'+path.stem, 'nutils')
for test in sorted(finder.find(module)):
if len(test.examples) == 0:
continue
Expand Down

0 comments on commit 0d5a714

Please sign in to comment.