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