mirror of https://github.com/python/cpython.git
- Added DocTestParser.parse(), which parses a docstring into Examples
and intervening text strings. - Removed DocTestParser.get_program(): use script_from_examples() instead. - Fixed bug in DocTestParser._INDENT_RE - Fixed bug in DocTestParser._min_indent - Moved _want_comment() to the utility function section
This commit is contained in:
parent
57d88e5abd
commit
00f8da77e7
221
Lib/doctest.py
221
Lib/doctest.py
|
@ -433,6 +433,14 @@ def _ellipsis_match(want, got):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _comment_line(line):
|
||||||
|
"Return a commented form of the given line"
|
||||||
|
line = line.rstrip()
|
||||||
|
if line:
|
||||||
|
return '# '+line
|
||||||
|
else:
|
||||||
|
return '#'
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
## 2. Example & DocTest
|
## 2. Example & DocTest
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -606,6 +614,45 @@ class DocTestParser:
|
||||||
# or contains a single comment.
|
# or contains a single comment.
|
||||||
_IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match
|
_IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match
|
||||||
|
|
||||||
|
def parse(self, string, name='<string>'):
|
||||||
|
"""
|
||||||
|
Divide the given string into examples and intervening text,
|
||||||
|
and return them as a list of alternating Examples and strings.
|
||||||
|
Line numbers for the Examples are 0-based. The optional
|
||||||
|
argument `name` is a name identifying this string, and is only
|
||||||
|
used for error messages.
|
||||||
|
"""
|
||||||
|
string = string.expandtabs()
|
||||||
|
# If all lines begin with the same indentation, then strip it.
|
||||||
|
min_indent = self._min_indent(string)
|
||||||
|
if min_indent > 0:
|
||||||
|
string = '\n'.join([l[min_indent:] for l in string.split('\n')])
|
||||||
|
|
||||||
|
output = []
|
||||||
|
charno, lineno = 0, 0
|
||||||
|
# Find all doctest examples in the string:
|
||||||
|
for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
|
||||||
|
# Add the pre-example text to `output`.
|
||||||
|
output.append(string[charno:m.start()])
|
||||||
|
# Update lineno (lines before this example)
|
||||||
|
lineno += string.count('\n', charno, m.start())
|
||||||
|
# Extract info from the regexp match.
|
||||||
|
(source, options, want, exc_msg) = \
|
||||||
|
self._parse_example(m, name, lineno)
|
||||||
|
# Create an Example, and add it to the list.
|
||||||
|
if not self._IS_BLANK_OR_COMMENT(source):
|
||||||
|
output.append( Example(source, want, exc_msg,
|
||||||
|
lineno=lineno,
|
||||||
|
indent=min_indent+len(m.group('indent')),
|
||||||
|
options=options) )
|
||||||
|
# Update lineno (lines inside this example)
|
||||||
|
lineno += string.count('\n', m.start(), m.end())
|
||||||
|
# Update charno.
|
||||||
|
charno = m.end()
|
||||||
|
# Add any remaining post-example text to `output`.
|
||||||
|
output.append(string[charno:])
|
||||||
|
return output
|
||||||
|
|
||||||
def get_doctest(self, string, globs, name, filename, lineno):
|
def get_doctest(self, string, globs, name, filename, lineno):
|
||||||
"""
|
"""
|
||||||
Extract all doctest examples from the given string, and
|
Extract all doctest examples from the given string, and
|
||||||
|
@ -628,124 +675,9 @@ def get_examples(self, string, name='<string>'):
|
||||||
|
|
||||||
The optional argument `name` is a name identifying this
|
The optional argument `name` is a name identifying this
|
||||||
string, and is only used for error messages.
|
string, and is only used for error messages.
|
||||||
|
|
||||||
>>> text = '''
|
|
||||||
... >>> x, y = 2, 3 # no output expected
|
|
||||||
... >>> if 1:
|
|
||||||
... ... print x
|
|
||||||
... ... print y
|
|
||||||
... 2
|
|
||||||
... 3
|
|
||||||
...
|
|
||||||
... Some text.
|
|
||||||
... >>> x+y
|
|
||||||
... 5
|
|
||||||
... '''
|
|
||||||
>>> for x in DocTestParser().get_examples(text):
|
|
||||||
... print (x.source, x.want, x.lineno)
|
|
||||||
('x, y = 2, 3 # no output expected\\n', '', 1)
|
|
||||||
('if 1:\\n print x\\n print y\\n', '2\\n3\\n', 2)
|
|
||||||
('x+y\\n', '5\\n', 9)
|
|
||||||
"""
|
"""
|
||||||
examples = []
|
return [x for x in self.parse(string, name)
|
||||||
charno, lineno = 0, 0
|
if isinstance(x, Example)]
|
||||||
# Find all doctest examples in the string:
|
|
||||||
for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
|
|
||||||
# Update lineno (lines before this example)
|
|
||||||
lineno += string.count('\n', charno, m.start())
|
|
||||||
# Extract source/want from the regexp match.
|
|
||||||
(source, want, exc_msg) = self._parse_example(m, name, lineno)
|
|
||||||
# Extract extra options from the source.
|
|
||||||
options = self._find_options(source, name, lineno)
|
|
||||||
# Create an Example, and add it to the list.
|
|
||||||
if not self._IS_BLANK_OR_COMMENT(source):
|
|
||||||
examples.append( Example(source, want, exc_msg,
|
|
||||||
lineno=lineno,
|
|
||||||
indent=len(m.group('indent')),
|
|
||||||
options=options) )
|
|
||||||
# Update lineno (lines inside this example)
|
|
||||||
lineno += string.count('\n', m.start(), m.end())
|
|
||||||
# Update charno.
|
|
||||||
charno = m.end()
|
|
||||||
return examples
|
|
||||||
|
|
||||||
def get_program(self, string, name="<string>"):
|
|
||||||
"""
|
|
||||||
Return an executable program from the given string, as a string.
|
|
||||||
|
|
||||||
The format of this isn't rigidly defined. In general, doctest
|
|
||||||
examples become the executable statements in the result, and
|
|
||||||
their expected outputs become comments, preceded by an \"#Expected:\"
|
|
||||||
comment. Everything else (text, comments, everything not part of
|
|
||||||
a doctest test) is also placed in comments.
|
|
||||||
|
|
||||||
The optional argument `name` is a name identifying this
|
|
||||||
string, and is only used for error messages.
|
|
||||||
|
|
||||||
>>> text = '''
|
|
||||||
... >>> x, y = 2, 3 # no output expected
|
|
||||||
... >>> if 1:
|
|
||||||
... ... print x
|
|
||||||
... ... print y
|
|
||||||
... 2
|
|
||||||
... 3
|
|
||||||
...
|
|
||||||
... Some text.
|
|
||||||
... >>> x+y
|
|
||||||
... 5
|
|
||||||
... '''
|
|
||||||
>>> print DocTestParser().get_program(text)
|
|
||||||
x, y = 2, 3 # no output expected
|
|
||||||
if 1:
|
|
||||||
print x
|
|
||||||
print y
|
|
||||||
# Expected:
|
|
||||||
## 2
|
|
||||||
## 3
|
|
||||||
#
|
|
||||||
# Some text.
|
|
||||||
x+y
|
|
||||||
# Expected:
|
|
||||||
## 5
|
|
||||||
"""
|
|
||||||
string = string.expandtabs()
|
|
||||||
# If all lines begin with the same indentation, then strip it.
|
|
||||||
min_indent = self._min_indent(string)
|
|
||||||
if min_indent > 0:
|
|
||||||
string = '\n'.join([l[min_indent:] for l in string.split('\n')])
|
|
||||||
|
|
||||||
output = []
|
|
||||||
charnum, lineno = 0, 0
|
|
||||||
# Find all doctest examples in the string:
|
|
||||||
for m in self._EXAMPLE_RE.finditer(string.expandtabs()):
|
|
||||||
# Add any text before this example, as a comment.
|
|
||||||
if m.start() > charnum:
|
|
||||||
lines = string[charnum:m.start()-1].split('\n')
|
|
||||||
output.extend([self._comment_line(l) for l in lines])
|
|
||||||
lineno += len(lines)
|
|
||||||
|
|
||||||
# Extract source/want from the regexp match.
|
|
||||||
(source, want, exc_msg) = self._parse_example(m, name, lineno)
|
|
||||||
# Display the source
|
|
||||||
output.append(source)
|
|
||||||
# Display the expected output, if any
|
|
||||||
if want:
|
|
||||||
output.append('# Expected:')
|
|
||||||
output.extend(['## '+l for l in want.split('\n')])
|
|
||||||
|
|
||||||
# Update the line number & char number.
|
|
||||||
lineno += string.count('\n', m.start(), m.end())
|
|
||||||
charnum = m.end()
|
|
||||||
# Add any remaining text, as comments.
|
|
||||||
output.extend([self._comment_line(l)
|
|
||||||
for l in string[charnum:].split('\n')])
|
|
||||||
# Trim junk on both ends.
|
|
||||||
while output and output[-1] == '#':
|
|
||||||
output.pop()
|
|
||||||
while output and output[0] == '#':
|
|
||||||
output.pop(0)
|
|
||||||
# Combine the output, and return it.
|
|
||||||
return '\n'.join(output)
|
|
||||||
|
|
||||||
def _parse_example(self, m, name, lineno):
|
def _parse_example(self, m, name, lineno):
|
||||||
"""
|
"""
|
||||||
|
@ -786,7 +718,10 @@ def _parse_example(self, m, name, lineno):
|
||||||
else:
|
else:
|
||||||
exc_msg = None
|
exc_msg = None
|
||||||
|
|
||||||
return source, want, exc_msg
|
# Extract options from the source.
|
||||||
|
options = self._find_options(source, name, lineno)
|
||||||
|
|
||||||
|
return source, options, want, exc_msg
|
||||||
|
|
||||||
# This regular expression looks for option directives in the
|
# This regular expression looks for option directives in the
|
||||||
# source code of an example. Option directives are comments
|
# source code of an example. Option directives are comments
|
||||||
|
@ -826,19 +761,15 @@ def _find_options(self, source, name, lineno):
|
||||||
|
|
||||||
# This regular expression finds the indentation of every non-blank
|
# This regular expression finds the indentation of every non-blank
|
||||||
# line in a string.
|
# line in a string.
|
||||||
_INDENT_RE = re.compile('^([ ]+)(?=\S)', re.MULTILINE)
|
_INDENT_RE = re.compile('^([ ]*)(?=\S)', re.MULTILINE)
|
||||||
|
|
||||||
def _min_indent(self, s):
|
def _min_indent(self, s):
|
||||||
"Return the minimum indentation of any non-blank line in `s`"
|
"Return the minimum indentation of any non-blank line in `s`"
|
||||||
return min([len(indent) for indent in self._INDENT_RE.findall(s)])
|
indents = [len(indent) for indent in self._INDENT_RE.findall(s)]
|
||||||
|
if len(indents) > 0:
|
||||||
def _comment_line(self, line):
|
return min(indents)
|
||||||
"Return a commented form of the given line"
|
|
||||||
line = line.rstrip()
|
|
||||||
if line:
|
|
||||||
return '# '+line
|
|
||||||
else:
|
else:
|
||||||
return '#'
|
return 0
|
||||||
|
|
||||||
def _check_prompt_blank(self, lines, indent, name, lineno):
|
def _check_prompt_blank(self, lines, indent, name, lineno):
|
||||||
"""
|
"""
|
||||||
|
@ -2319,25 +2250,31 @@ def script_from_examples(s):
|
||||||
if 0:
|
if 0:
|
||||||
blah
|
blah
|
||||||
blah
|
blah
|
||||||
<BLANKLINE>
|
|
||||||
#
|
#
|
||||||
# Ho hum
|
# Ho hum
|
||||||
"""
|
"""
|
||||||
|
output = []
|
||||||
return DocTestParser().get_program(s)
|
for piece in DocTestParser().parse(s):
|
||||||
|
if isinstance(piece, Example):
|
||||||
def _want_comment(example):
|
# Add the example's source code (strip trailing NL)
|
||||||
"""
|
output.append(piece.source[:-1])
|
||||||
Return a comment containing the expected output for the given example.
|
# Add the expected output:
|
||||||
"""
|
want = piece.want
|
||||||
# Return the expected output, if any
|
|
||||||
want = example.want
|
|
||||||
if want:
|
if want:
|
||||||
if want[-1] == '\n':
|
output.append('# Expected:')
|
||||||
want = want[:-1]
|
output += ['## '+l for l in want.split('\n')[:-1]]
|
||||||
want = "\n# ".join(want.split("\n"))
|
else:
|
||||||
want = "\n# Expected:\n# %s" % want
|
# Add non-example text.
|
||||||
return want
|
output += [_comment_line(l)
|
||||||
|
for l in piece.split('\n')[:-1]]
|
||||||
|
|
||||||
|
# Trim junk on both ends.
|
||||||
|
while output and output[-1] == '#':
|
||||||
|
output.pop()
|
||||||
|
while output and output[0] == '#':
|
||||||
|
output.pop(0)
|
||||||
|
# Combine the output, and return it.
|
||||||
|
return '\n'.join(output)
|
||||||
|
|
||||||
def testsource(module, name):
|
def testsource(module, name):
|
||||||
"""Extract the test sources from a doctest docstring as a script.
|
"""Extract the test sources from a doctest docstring as a script.
|
||||||
|
|
|
@ -283,7 +283,7 @@ def test_DocTest(): r"""
|
||||||
... '''
|
... '''
|
||||||
>>> parser.get_doctest(docstring, globs, 'some_test', 'filename', 0)
|
>>> parser.get_doctest(docstring, globs, 'some_test', 'filename', 0)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
ValueError: line 4 of the docstring for some_test has inconsistent leading whitespace: ' indentation'
|
ValueError: line 4 of the docstring for some_test has inconsistent leading whitespace: 'indentation'
|
||||||
|
|
||||||
If the docstring contains inconsistent leading whitespace on
|
If the docstring contains inconsistent leading whitespace on
|
||||||
continuation lines, then `DocTest` will raise a ValueError:
|
continuation lines, then `DocTest` will raise a ValueError:
|
||||||
|
@ -295,7 +295,7 @@ def test_DocTest(): r"""
|
||||||
... '''
|
... '''
|
||||||
>>> parser.get_doctest(docstring, globs, 'some_test', 'filename', 0)
|
>>> parser.get_doctest(docstring, globs, 'some_test', 'filename', 0)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
ValueError: line 2 of the docstring for some_test has inconsistent leading whitespace: ' ... 2)'
|
ValueError: line 2 of the docstring for some_test has inconsistent leading whitespace: '... 2)'
|
||||||
|
|
||||||
If there's no blank space after a PS1 prompt ('>>>'), then `DocTest`
|
If there's no blank space after a PS1 prompt ('>>>'), then `DocTest`
|
||||||
will raise a ValueError:
|
will raise a ValueError:
|
||||||
|
@ -553,6 +553,61 @@ def test_DocTestFinder(): r"""
|
||||||
[1, 9, 12]
|
[1, 9, 12]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def test_DocTestParser(): r"""
|
||||||
|
Unit tests for the `DocTestParser` class.
|
||||||
|
|
||||||
|
DocTestParser is used to parse docstrings containing doctest examples.
|
||||||
|
|
||||||
|
The `parse` method divides a docstring into examples and intervening
|
||||||
|
text:
|
||||||
|
|
||||||
|
>>> s = '''
|
||||||
|
... >>> x, y = 2, 3 # no output expected
|
||||||
|
... >>> if 1:
|
||||||
|
... ... print x
|
||||||
|
... ... print y
|
||||||
|
... 2
|
||||||
|
... 3
|
||||||
|
...
|
||||||
|
... Some text.
|
||||||
|
... >>> x+y
|
||||||
|
... 5
|
||||||
|
... '''
|
||||||
|
>>> parser = doctest.DocTestParser()
|
||||||
|
>>> for piece in parser.parse(s):
|
||||||
|
... if isinstance(piece, doctest.Example):
|
||||||
|
... print 'Example:', (piece.source, piece.want, piece.lineno)
|
||||||
|
... else:
|
||||||
|
... print ' Text:', `piece`
|
||||||
|
Text: '\n'
|
||||||
|
Example: ('x, y = 2, 3 # no output expected\n', '', 1)
|
||||||
|
Text: ''
|
||||||
|
Example: ('if 1:\n print x\n print y\n', '2\n3\n', 2)
|
||||||
|
Text: '\nSome text.\n'
|
||||||
|
Example: ('x+y\n', '5\n', 9)
|
||||||
|
Text: ''
|
||||||
|
|
||||||
|
The `get_examples` method returns just the examples:
|
||||||
|
|
||||||
|
>>> for piece in parser.get_examples(s):
|
||||||
|
... print (piece.source, piece.want, piece.lineno)
|
||||||
|
('x, y = 2, 3 # no output expected\n', '', 1)
|
||||||
|
('if 1:\n print x\n print y\n', '2\n3\n', 2)
|
||||||
|
('x+y\n', '5\n', 9)
|
||||||
|
|
||||||
|
The `get_doctest` method creates a Test from the examples, along with the
|
||||||
|
given arguments:
|
||||||
|
|
||||||
|
>>> test = parser.get_doctest(s, {}, 'name', 'filename', lineno=5)
|
||||||
|
>>> (test.name, test.filename, test.lineno)
|
||||||
|
('name', 'filename', 5)
|
||||||
|
>>> for piece in test.examples:
|
||||||
|
... print (piece.source, piece.want, piece.lineno)
|
||||||
|
('x, y = 2, 3 # no output expected\n', '', 1)
|
||||||
|
('if 1:\n print x\n print y\n', '2\n3\n', 2)
|
||||||
|
('x+y\n', '5\n', 9)
|
||||||
|
"""
|
||||||
|
|
||||||
class test_DocTestRunner:
|
class test_DocTestRunner:
|
||||||
def basics(): r"""
|
def basics(): r"""
|
||||||
Unit tests for the `DocTestRunner` class.
|
Unit tests for the `DocTestRunner` class.
|
||||||
|
|
Loading…
Reference in New Issue