Format: text/x-python
⬇ Download File (11.6 KB)
   1  """Nose Plugin that supports IPython doctests.
   2  
   3  Limitations:
   4  
   5  - When generating examples for use as doctests, make sure that you have
   6    pretty-printing OFF.  This can be done either by setting the
   7    ``PlainTextFormatter.pprint`` option in your configuration file to  False, or
   8    by interactively disabling it with  %Pprint.  This is required so that IPython
   9    output matches that of normal Python, which is used by doctest for internal
  10    execution.
  11  
  12  - Do not rely on specific prompt numbers for results (such as using
  13    '_34==True', for example).  For IPython tests run via an external process the
  14    prompt numbers may be different, and IPython tests run as normal python code
  15    won't even have these special _NN variables set at all.
  16  """
  17  
  18  #-----------------------------------------------------------------------------
  19  # Module imports
  20  
  21  # From the standard library
  22  import doctest
  23  import logging
  24  import re
  25  
  26  from testpath import modified_env
  27  
  28  #-----------------------------------------------------------------------------
  29  # Module globals and other constants
  30  #-----------------------------------------------------------------------------
  31  
  32  log = logging.getLogger(__name__)
  33  
  34  
  35  #-----------------------------------------------------------------------------
  36  # Classes and functions
  37  #-----------------------------------------------------------------------------
  38  
  39  
  40  class DocTestFinder(doctest.DocTestFinder):
  41      def _get_test(self, obj, name, module, globs, source_lines):
  42          test = super()._get_test(obj, name, module, globs, source_lines)
  43  
  44          if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
  45              for example in test.examples:
  46                  example.options[doctest.SKIP] = True
  47  
  48          return test
  49  
  50  
  51  class IPDoctestOutputChecker(doctest.OutputChecker):
  52      """Second-chance checker with support for random tests.
  53  
  54      If the default comparison doesn't pass, this checker looks in the expected
  55      output string for flags that tell us to ignore the output.
  56      """
  57  
  58      random_re = re.compile(r'#\s*random\s+')
  59  
  60      def check_output(self, want, got, optionflags):
  61          """Check output, accepting special markers embedded in the output.
  62  
  63          If the output didn't pass the default validation but the special string
  64          '#random' is included, we accept it."""
  65  
  66          # Let the original tester verify first, in case people have valid tests
  67          # that happen to have a comment saying '#random' embedded in.
  68          ret = doctest.OutputChecker.check_output(self, want, got,
  69                                                   optionflags)
  70          if not ret and self.random_re.search(want):
  71              # print('RANDOM OK:',want, file=sys.stderr)  # dbg
  72              return True
  73  
  74          return ret
  75  
  76  
  77  # A simple subclassing of the original with a different class name, so we can
  78  # distinguish and treat differently IPython examples from pure python ones.
  79  class IPExample(doctest.Example): pass
  80  
  81  
  82  class IPDocTestParser(doctest.DocTestParser):
  83      """
  84      A class used to parse strings containing doctest examples.
  85  
  86      Note: This is a version modified to properly recognize IPython input and
  87      convert any IPython examples into valid Python ones.
  88      """
  89      # This regular expression is used to find doctest examples in a
  90      # string.  It defines three groups: `source` is the source code
  91      # (including leading indentation and prompts); `indent` is the
  92      # indentation of the first (PS1) line of the source code; and
  93      # `want` is the expected output (including leading indentation).
  94  
  95      # Classic Python prompts or default IPython ones
  96      _PS1_PY = r'>>>'
  97      _PS2_PY = r'\.\.\.'
  98  
  99      _PS1_IP = r'In\ \[\d+\]:'
 100      _PS2_IP = r'\ \ \ \.\.\.+:'
 101  
 102      _RE_TPL = r'''
 103          # Source consists of a PS1 line followed by zero or more PS2 lines.
 104          (?P<source>
 105              (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*)    # PS1 line
 106              (?:\n           [ ]*  (?P<ps2> %s) .*)*)  # PS2 lines
 107          \n? # a newline
 108          # Want consists of any non-blank lines that do not start with PS1.
 109          (?P<want> (?:(?![ ]*$)    # Not a blank line
 110                       (?![ ]*%s)   # Not a line starting with PS1
 111                       (?![ ]*%s)   # Not a line starting with PS2
 112                       .*$\n?       # But any other line
 113                    )*)
 114                    '''
 115  
 116      _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
 117                                   re.MULTILINE | re.VERBOSE)
 118  
 119      _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
 120                                   re.MULTILINE | re.VERBOSE)
 121  
 122      # Mark a test as being fully random.  In this case, we simply append the
 123      # random marker ('#random') to each individual example's output.  This way
 124      # we don't need to modify any other code.
 125      _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
 126  
 127      def ip2py(self,source):
 128          """Convert input IPython source into valid Python."""
 129          block = _ip.input_transformer_manager.transform_cell(source)
 130          if len(block.splitlines()) == 1:
 131              return _ip.prefilter(block)
 132          else:
 133              return block
 134  
 135      def parse(self, string, name='<string>'):
 136          """
 137          Divide the given string into examples and intervening text,
 138          and return them as a list of alternating Examples and strings.
 139          Line numbers for the Examples are 0-based.  The optional
 140          argument `name` is a name identifying this string, and is only
 141          used for error messages.
 142          """
 143  
 144          # print('Parse string:\n',string)  # dbg
 145  
 146          string = string.expandtabs()
 147          # If all lines begin with the same indentation, then strip it.
 148          min_indent = self._min_indent(string)
 149          if min_indent > 0:
 150              string = '\n'.join([l[min_indent:] for l in string.split('\n')])
 151  
 152          output = []
 153          charno, lineno = 0, 0
 154  
 155          # We make 'all random' tests by adding the '# random' mark to every
 156          # block of output in the test.
 157          if self._RANDOM_TEST.search(string):
 158              random_marker = '\n# random'
 159          else:
 160              random_marker = ''
 161  
 162          # Whether to convert the input from ipython to python syntax
 163          ip2py = False
 164          # Find all doctest examples in the string.  First, try them as Python
 165          # examples, then as IPython ones
 166          terms = list(self._EXAMPLE_RE_PY.finditer(string))
 167          if terms:
 168              # Normal Python example
 169              Example = doctest.Example
 170          else:
 171              # It's an ipython example.
 172              terms = list(self._EXAMPLE_RE_IP.finditer(string))
 173              Example = IPExample
 174              ip2py = True
 175  
 176          for m in terms:
 177              # Add the pre-example text to `output`.
 178              output.append(string[charno:m.start()])
 179              # Update lineno (lines before this example)
 180              lineno += string.count('\n', charno, m.start())
 181              # Extract info from the regexp match.
 182              (source, options, want, exc_msg) = \
 183                       self._parse_example(m, name, lineno,ip2py)
 184  
 185              # Append the random-output marker (it defaults to empty in most
 186              # cases, it's only non-empty for 'all-random' tests):
 187              want += random_marker
 188  
 189              # Create an Example, and add it to the list.
 190              if not self._IS_BLANK_OR_COMMENT(source):
 191                  output.append(Example(source, want, exc_msg,
 192                                        lineno=lineno,
 193                                        indent=min_indent+len(m.group('indent')),
 194                                        options=options))
 195              # Update lineno (lines inside this example)
 196              lineno += string.count('\n', m.start(), m.end())
 197              # Update charno.
 198              charno = m.end()
 199          # Add any remaining post-example text to `output`.
 200          output.append(string[charno:])
 201          return output
 202  
 203      def _parse_example(self, m, name, lineno,ip2py=False):
 204          """
 205          Given a regular expression match from `_EXAMPLE_RE` (`m`),
 206          return a pair `(source, want)`, where `source` is the matched
 207          example's source code (with prompts and indentation stripped);
 208          and `want` is the example's expected output (with indentation
 209          stripped).
 210  
 211          `name` is the string's name, and `lineno` is the line number
 212          where the example starts; both are used for error messages.
 213  
 214          Optional:
 215          `ip2py`: if true, filter the input via IPython to convert the syntax
 216          into valid python.
 217          """
 218  
 219          # Get the example's indentation level.
 220          indent = len(m.group('indent'))
 221  
 222          # Divide source into lines; check that they're properly
 223          # indented; and then strip their indentation & prompts.
 224          source_lines = m.group('source').split('\n')
 225  
 226          # We're using variable-length input prompts
 227          ps1 = m.group('ps1')
 228          ps2 = m.group('ps2')
 229          ps1_len = len(ps1)
 230  
 231          self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
 232          if ps2:
 233              self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
 234  
 235          source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
 236  
 237          if ip2py:
 238              # Convert source input from IPython into valid Python syntax
 239              source = self.ip2py(source)
 240  
 241          # Divide want into lines; check that it's properly indented; and
 242          # then strip the indentation.  Spaces before the last newline should
 243          # be preserved, so plain rstrip() isn't good enough.
 244          want = m.group('want')
 245          want_lines = want.split('\n')
 246          if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
 247              del want_lines[-1]  # forget final newline & spaces after it
 248          self._check_prefix(want_lines, ' '*indent, name,
 249                             lineno + len(source_lines))
 250  
 251          # Remove ipython output prompt that might be present in the first line
 252          want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
 253  
 254          want = '\n'.join([wl[indent:] for wl in want_lines])
 255  
 256          # If `want` contains a traceback message, then extract it.
 257          m = self._EXCEPTION_RE.match(want)
 258          if m:
 259              exc_msg = m.group('msg')
 260          else:
 261              exc_msg = None
 262  
 263          # Extract options from the source.
 264          options = self._find_options(source, name, lineno)
 265  
 266          return source, options, want, exc_msg
 267  
 268      def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
 269          """
 270          Given the lines of a source string (including prompts and
 271          leading indentation), check to make sure that every prompt is
 272          followed by a space character.  If any line is not followed by
 273          a space character, then raise ValueError.
 274  
 275          Note: IPython-modified version which takes the input prompt length as a
 276          parameter, so that prompts of variable length can be dealt with.
 277          """
 278          space_idx = indent+ps1_len
 279          min_len = space_idx+1
 280          for i, line in enumerate(lines):
 281              if len(line) >=  min_len and line[space_idx] != ' ':
 282                  raise ValueError('line %r of the docstring for %s '
 283                                   'lacks blank after %s: %r' %
 284                                   (lineno+i+1, name,
 285                                    line[indent:space_idx], line))
 286  
 287  
 288  SKIP = doctest.register_optionflag('SKIP')
 289  
 290  
 291  class IPDocTestRunner(doctest.DocTestRunner,object):
 292      """Test runner that synchronizes the IPython namespace with test globals.
 293      """
 294  
 295      def run(self, test, compileflags=None, out=None, clear_globs=True):
 296          # Override terminal size to standardise traceback format
 297          with modified_env({'COLUMNS': '80', 'LINES': '24'}):
 298              return super(IPDocTestRunner,self).run(test,
 299                                                     compileflags,out,clear_globs)
 300