You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			300 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			300 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
"""Nose Plugin that supports IPython doctests.
 | 
						|
 | 
						|
Limitations:
 | 
						|
 | 
						|
- When generating examples for use as doctests, make sure that you have
 | 
						|
  pretty-printing OFF.  This can be done either by setting the
 | 
						|
  ``PlainTextFormatter.pprint`` option in your configuration file to  False, or
 | 
						|
  by interactively disabling it with  %Pprint.  This is required so that IPython
 | 
						|
  output matches that of normal Python, which is used by doctest for internal
 | 
						|
  execution.
 | 
						|
 | 
						|
- Do not rely on specific prompt numbers for results (such as using
 | 
						|
  '_34==True', for example).  For IPython tests run via an external process the
 | 
						|
  prompt numbers may be different, and IPython tests run as normal python code
 | 
						|
  won't even have these special _NN variables set at all.
 | 
						|
"""
 | 
						|
 | 
						|
#-----------------------------------------------------------------------------
 | 
						|
# Module imports
 | 
						|
 | 
						|
# From the standard library
 | 
						|
import doctest
 | 
						|
import logging
 | 
						|
import re
 | 
						|
 | 
						|
from testpath import modified_env
 | 
						|
 | 
						|
#-----------------------------------------------------------------------------
 | 
						|
# Module globals and other constants
 | 
						|
#-----------------------------------------------------------------------------
 | 
						|
 | 
						|
log = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
#-----------------------------------------------------------------------------
 | 
						|
# Classes and functions
 | 
						|
#-----------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
class DocTestFinder(doctest.DocTestFinder):
 | 
						|
    def _get_test(self, obj, name, module, globs, source_lines):
 | 
						|
        test = super()._get_test(obj, name, module, globs, source_lines)
 | 
						|
 | 
						|
        if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
 | 
						|
            for example in test.examples:
 | 
						|
                example.options[doctest.SKIP] = True
 | 
						|
 | 
						|
        return test
 | 
						|
 | 
						|
 | 
						|
class IPDoctestOutputChecker(doctest.OutputChecker):
 | 
						|
    """Second-chance checker with support for random tests.
 | 
						|
 | 
						|
    If the default comparison doesn't pass, this checker looks in the expected
 | 
						|
    output string for flags that tell us to ignore the output.
 | 
						|
    """
 | 
						|
 | 
						|
    random_re = re.compile(r'#\s*random\s+')
 | 
						|
 | 
						|
    def check_output(self, want, got, optionflags):
 | 
						|
        """Check output, accepting special markers embedded in the output.
 | 
						|
 | 
						|
        If the output didn't pass the default validation but the special string
 | 
						|
        '#random' is included, we accept it."""
 | 
						|
 | 
						|
        # Let the original tester verify first, in case people have valid tests
 | 
						|
        # that happen to have a comment saying '#random' embedded in.
 | 
						|
        ret = doctest.OutputChecker.check_output(self, want, got,
 | 
						|
                                                 optionflags)
 | 
						|
        if not ret and self.random_re.search(want):
 | 
						|
            # print('RANDOM OK:',want, file=sys.stderr)  # dbg
 | 
						|
            return True
 | 
						|
 | 
						|
        return ret
 | 
						|
 | 
						|
 | 
						|
# A simple subclassing of the original with a different class name, so we can
 | 
						|
# distinguish and treat differently IPython examples from pure python ones.
 | 
						|
class IPExample(doctest.Example): pass
 | 
						|
 | 
						|
 | 
						|
class IPDocTestParser(doctest.DocTestParser):
 | 
						|
    """
 | 
						|
    A class used to parse strings containing doctest examples.
 | 
						|
 | 
						|
    Note: This is a version modified to properly recognize IPython input and
 | 
						|
    convert any IPython examples into valid Python ones.
 | 
						|
    """
 | 
						|
    # This regular expression is used to find doctest examples in a
 | 
						|
    # string.  It defines three groups: `source` is the source code
 | 
						|
    # (including leading indentation and prompts); `indent` is the
 | 
						|
    # indentation of the first (PS1) line of the source code; and
 | 
						|
    # `want` is the expected output (including leading indentation).
 | 
						|
 | 
						|
    # Classic Python prompts or default IPython ones
 | 
						|
    _PS1_PY = r'>>>'
 | 
						|
    _PS2_PY = r'\.\.\.'
 | 
						|
 | 
						|
    _PS1_IP = r'In\ \[\d+\]:'
 | 
						|
    _PS2_IP = r'\ \ \ \.\.\.+:'
 | 
						|
 | 
						|
    _RE_TPL = r'''
 | 
						|
        # Source consists of a PS1 line followed by zero or more PS2 lines.
 | 
						|
        (?P<source>
 | 
						|
            (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*)    # PS1 line
 | 
						|
            (?:\n           [ ]*  (?P<ps2> %s) .*)*)  # PS2 lines
 | 
						|
        \n? # a newline
 | 
						|
        # Want consists of any non-blank lines that do not start with PS1.
 | 
						|
        (?P<want> (?:(?![ ]*$)    # Not a blank line
 | 
						|
                     (?![ ]*%s)   # Not a line starting with PS1
 | 
						|
                     (?![ ]*%s)   # Not a line starting with PS2
 | 
						|
                     .*$\n?       # But any other line
 | 
						|
                  )*)
 | 
						|
                  '''
 | 
						|
 | 
						|
    _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
 | 
						|
                                 re.MULTILINE | re.VERBOSE)
 | 
						|
 | 
						|
    _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
 | 
						|
                                 re.MULTILINE | re.VERBOSE)
 | 
						|
 | 
						|
    # Mark a test as being fully random.  In this case, we simply append the
 | 
						|
    # random marker ('#random') to each individual example's output.  This way
 | 
						|
    # we don't need to modify any other code.
 | 
						|
    _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
 | 
						|
 | 
						|
    def ip2py(self,source):
 | 
						|
        """Convert input IPython source into valid Python."""
 | 
						|
        block = _ip.input_transformer_manager.transform_cell(source)
 | 
						|
        if len(block.splitlines()) == 1:
 | 
						|
            return _ip.prefilter(block)
 | 
						|
        else:
 | 
						|
            return block
 | 
						|
 | 
						|
    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.
 | 
						|
        """
 | 
						|
 | 
						|
        # print('Parse string:\n',string)  # dbg
 | 
						|
 | 
						|
        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
 | 
						|
 | 
						|
        # We make 'all random' tests by adding the '# random' mark to every
 | 
						|
        # block of output in the test.
 | 
						|
        if self._RANDOM_TEST.search(string):
 | 
						|
            random_marker = '\n# random'
 | 
						|
        else:
 | 
						|
            random_marker = ''
 | 
						|
 | 
						|
        # Whether to convert the input from ipython to python syntax
 | 
						|
        ip2py = False
 | 
						|
        # Find all doctest examples in the string.  First, try them as Python
 | 
						|
        # examples, then as IPython ones
 | 
						|
        terms = list(self._EXAMPLE_RE_PY.finditer(string))
 | 
						|
        if terms:
 | 
						|
            # Normal Python example
 | 
						|
            Example = doctest.Example
 | 
						|
        else:
 | 
						|
            # It's an ipython example.
 | 
						|
            terms = list(self._EXAMPLE_RE_IP.finditer(string))
 | 
						|
            Example = IPExample
 | 
						|
            ip2py = True
 | 
						|
 | 
						|
        for m in terms:
 | 
						|
            # 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,ip2py)
 | 
						|
 | 
						|
            # Append the random-output marker (it defaults to empty in most
 | 
						|
            # cases, it's only non-empty for 'all-random' tests):
 | 
						|
            want += random_marker
 | 
						|
 | 
						|
            # 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 _parse_example(self, m, name, lineno,ip2py=False):
 | 
						|
        """
 | 
						|
        Given a regular expression match from `_EXAMPLE_RE` (`m`),
 | 
						|
        return a pair `(source, want)`, where `source` is the matched
 | 
						|
        example's source code (with prompts and indentation stripped);
 | 
						|
        and `want` is the example's expected output (with indentation
 | 
						|
        stripped).
 | 
						|
 | 
						|
        `name` is the string's name, and `lineno` is the line number
 | 
						|
        where the example starts; both are used for error messages.
 | 
						|
 | 
						|
        Optional:
 | 
						|
        `ip2py`: if true, filter the input via IPython to convert the syntax
 | 
						|
        into valid python.
 | 
						|
        """
 | 
						|
 | 
						|
        # Get the example's indentation level.
 | 
						|
        indent = len(m.group('indent'))
 | 
						|
 | 
						|
        # Divide source into lines; check that they're properly
 | 
						|
        # indented; and then strip their indentation & prompts.
 | 
						|
        source_lines = m.group('source').split('\n')
 | 
						|
 | 
						|
        # We're using variable-length input prompts
 | 
						|
        ps1 = m.group('ps1')
 | 
						|
        ps2 = m.group('ps2')
 | 
						|
        ps1_len = len(ps1)
 | 
						|
 | 
						|
        self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
 | 
						|
        if ps2:
 | 
						|
            self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
 | 
						|
 | 
						|
        source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
 | 
						|
 | 
						|
        if ip2py:
 | 
						|
            # Convert source input from IPython into valid Python syntax
 | 
						|
            source = self.ip2py(source)
 | 
						|
 | 
						|
        # Divide want into lines; check that it's properly indented; and
 | 
						|
        # then strip the indentation.  Spaces before the last newline should
 | 
						|
        # be preserved, so plain rstrip() isn't good enough.
 | 
						|
        want = m.group('want')
 | 
						|
        want_lines = want.split('\n')
 | 
						|
        if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
 | 
						|
            del want_lines[-1]  # forget final newline & spaces after it
 | 
						|
        self._check_prefix(want_lines, ' '*indent, name,
 | 
						|
                           lineno + len(source_lines))
 | 
						|
 | 
						|
        # Remove ipython output prompt that might be present in the first line
 | 
						|
        want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
 | 
						|
 | 
						|
        want = '\n'.join([wl[indent:] for wl in want_lines])
 | 
						|
 | 
						|
        # If `want` contains a traceback message, then extract it.
 | 
						|
        m = self._EXCEPTION_RE.match(want)
 | 
						|
        if m:
 | 
						|
            exc_msg = m.group('msg')
 | 
						|
        else:
 | 
						|
            exc_msg = None
 | 
						|
 | 
						|
        # Extract options from the source.
 | 
						|
        options = self._find_options(source, name, lineno)
 | 
						|
 | 
						|
        return source, options, want, exc_msg
 | 
						|
 | 
						|
    def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
 | 
						|
        """
 | 
						|
        Given the lines of a source string (including prompts and
 | 
						|
        leading indentation), check to make sure that every prompt is
 | 
						|
        followed by a space character.  If any line is not followed by
 | 
						|
        a space character, then raise ValueError.
 | 
						|
 | 
						|
        Note: IPython-modified version which takes the input prompt length as a
 | 
						|
        parameter, so that prompts of variable length can be dealt with.
 | 
						|
        """
 | 
						|
        space_idx = indent+ps1_len
 | 
						|
        min_len = space_idx+1
 | 
						|
        for i, line in enumerate(lines):
 | 
						|
            if len(line) >=  min_len and line[space_idx] != ' ':
 | 
						|
                raise ValueError('line %r of the docstring for %s '
 | 
						|
                                 'lacks blank after %s: %r' %
 | 
						|
                                 (lineno+i+1, name,
 | 
						|
                                  line[indent:space_idx], line))
 | 
						|
 | 
						|
 | 
						|
SKIP = doctest.register_optionflag('SKIP')
 | 
						|
 | 
						|
 | 
						|
class IPDocTestRunner(doctest.DocTestRunner):
 | 
						|
    """Test runner that synchronizes the IPython namespace with test globals.
 | 
						|
    """
 | 
						|
 | 
						|
    def run(self, test, compileflags=None, out=None, clear_globs=True):
 | 
						|
        # Override terminal size to standardise traceback format
 | 
						|
        with modified_env({'COLUMNS': '80', 'LINES': '24'}):
 | 
						|
            return super(IPDocTestRunner,self).run(test,
 | 
						|
                                                   compileflags,out,clear_globs)
 |