##// END OF EJS Templates
Merge pull request #13329 from Kojoley/ipdoctest-cleanup...
Matthias Bussonnier -
r27160:335a1eb2 merge
parent child Browse files
Show More
@@ -1,346 +1,300 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 os
24 import os
25 import re
25 import re
26
26
27 from testpath import modified_env
27 from testpath import modified_env
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Module globals and other constants
30 # Module globals and other constants
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Classes and functions
37 # Classes and functions
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40
40
41 class DocTestFinder(doctest.DocTestFinder):
41 class DocTestFinder(doctest.DocTestFinder):
42 def _get_test(self, obj, name, module, globs, source_lines):
42 def _get_test(self, obj, name, module, globs, source_lines):
43 test = super()._get_test(obj, name, module, globs, source_lines)
43 test = super()._get_test(obj, name, module, globs, source_lines)
44
44
45 if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
45 if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
46 for example in test.examples:
46 for example in test.examples:
47 example.options[doctest.SKIP] = True
47 example.options[doctest.SKIP] = True
48
48
49 return test
49 return test
50
50
51
51
52 class IPDoctestOutputChecker(doctest.OutputChecker):
52 class IPDoctestOutputChecker(doctest.OutputChecker):
53 """Second-chance checker with support for random tests.
53 """Second-chance checker with support for random tests.
54
54
55 If the default comparison doesn't pass, this checker looks in the expected
55 If the default comparison doesn't pass, this checker looks in the expected
56 output string for flags that tell us to ignore the output.
56 output string for flags that tell us to ignore the output.
57 """
57 """
58
58
59 random_re = re.compile(r'#\s*random\s+')
59 random_re = re.compile(r'#\s*random\s+')
60
60
61 def check_output(self, want, got, optionflags):
61 def check_output(self, want, got, optionflags):
62 """Check output, accepting special markers embedded in the output.
62 """Check output, accepting special markers embedded in the output.
63
63
64 If the output didn't pass the default validation but the special string
64 If the output didn't pass the default validation but the special string
65 '#random' is included, we accept it."""
65 '#random' is included, we accept it."""
66
66
67 # Let the original tester verify first, in case people have valid tests
67 # Let the original tester verify first, in case people have valid tests
68 # that happen to have a comment saying '#random' embedded in.
68 # that happen to have a comment saying '#random' embedded in.
69 ret = doctest.OutputChecker.check_output(self, want, got,
69 ret = doctest.OutputChecker.check_output(self, want, got,
70 optionflags)
70 optionflags)
71 if not ret and self.random_re.search(want):
71 if not ret and self.random_re.search(want):
72 #print >> sys.stderr, 'RANDOM OK:',want # dbg
72 #print >> sys.stderr, 'RANDOM OK:',want # dbg
73 return True
73 return True
74
74
75 return ret
75 return ret
76
76
77
77
78 # A simple subclassing of the original with a different class name, so we can
78 # A simple subclassing of the original with a different class name, so we can
79 # distinguish and treat differently IPython examples from pure python ones.
79 # distinguish and treat differently IPython examples from pure python ones.
80 class IPExample(doctest.Example): pass
80 class IPExample(doctest.Example): pass
81
81
82
82
83 class IPExternalExample(doctest.Example):
84 """Doctest examples to be run in an external process."""
85
86 def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
87 options=None):
88 # Parent constructor
89 doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
90
91 # An EXTRA newline is needed to prevent pexpect hangs
92 self.source += '\n'
93
94
95 class IPDocTestParser(doctest.DocTestParser):
83 class IPDocTestParser(doctest.DocTestParser):
96 """
84 """
97 A class used to parse strings containing doctest examples.
85 A class used to parse strings containing doctest examples.
98
86
99 Note: This is a version modified to properly recognize IPython input and
87 Note: This is a version modified to properly recognize IPython input and
100 convert any IPython examples into valid Python ones.
88 convert any IPython examples into valid Python ones.
101 """
89 """
102 # This regular expression is used to find doctest examples in a
90 # This regular expression is used to find doctest examples in a
103 # string. It defines three groups: `source` is the source code
91 # string. It defines three groups: `source` is the source code
104 # (including leading indentation and prompts); `indent` is the
92 # (including leading indentation and prompts); `indent` is the
105 # indentation of the first (PS1) line of the source code; and
93 # indentation of the first (PS1) line of the source code; and
106 # `want` is the expected output (including leading indentation).
94 # `want` is the expected output (including leading indentation).
107
95
108 # Classic Python prompts or default IPython ones
96 # Classic Python prompts or default IPython ones
109 _PS1_PY = r'>>>'
97 _PS1_PY = r'>>>'
110 _PS2_PY = r'\.\.\.'
98 _PS2_PY = r'\.\.\.'
111
99
112 _PS1_IP = r'In\ \[\d+\]:'
100 _PS1_IP = r'In\ \[\d+\]:'
113 _PS2_IP = r'\ \ \ \.\.\.+:'
101 _PS2_IP = r'\ \ \ \.\.\.+:'
114
102
115 _RE_TPL = r'''
103 _RE_TPL = r'''
116 # Source consists of a PS1 line followed by zero or more PS2 lines.
104 # Source consists of a PS1 line followed by zero or more PS2 lines.
117 (?P<source>
105 (?P<source>
118 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
106 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
119 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
107 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
120 \n? # a newline
108 \n? # a newline
121 # Want consists of any non-blank lines that do not start with PS1.
109 # Want consists of any non-blank lines that do not start with PS1.
122 (?P<want> (?:(?![ ]*$) # Not a blank line
110 (?P<want> (?:(?![ ]*$) # Not a blank line
123 (?![ ]*%s) # Not a line starting with PS1
111 (?![ ]*%s) # Not a line starting with PS1
124 (?![ ]*%s) # Not a line starting with PS2
112 (?![ ]*%s) # Not a line starting with PS2
125 .*$\n? # But any other line
113 .*$\n? # But any other line
126 )*)
114 )*)
127 '''
115 '''
128
116
129 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
117 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
130 re.MULTILINE | re.VERBOSE)
118 re.MULTILINE | re.VERBOSE)
131
119
132 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
120 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
133 re.MULTILINE | re.VERBOSE)
121 re.MULTILINE | re.VERBOSE)
134
122
135 # Mark a test as being fully random. In this case, we simply append the
123 # Mark a test as being fully random. In this case, we simply append the
136 # random marker ('#random') to each individual example's output. This way
124 # random marker ('#random') to each individual example's output. This way
137 # we don't need to modify any other code.
125 # we don't need to modify any other code.
138 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
126 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
139
127
140 # Mark tests to be executed in an external process - currently unsupported.
141 _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
142
143 def ip2py(self,source):
128 def ip2py(self,source):
144 """Convert input IPython source into valid Python."""
129 """Convert input IPython source into valid Python."""
145 block = _ip.input_transformer_manager.transform_cell(source)
130 block = _ip.input_transformer_manager.transform_cell(source)
146 if len(block.splitlines()) == 1:
131 if len(block.splitlines()) == 1:
147 return _ip.prefilter(block)
132 return _ip.prefilter(block)
148 else:
133 else:
149 return block
134 return block
150
135
151 def parse(self, string, name='<string>'):
136 def parse(self, string, name='<string>'):
152 """
137 """
153 Divide the given string into examples and intervening text,
138 Divide the given string into examples and intervening text,
154 and return them as a list of alternating Examples and strings.
139 and return them as a list of alternating Examples and strings.
155 Line numbers for the Examples are 0-based. The optional
140 Line numbers for the Examples are 0-based. The optional
156 argument `name` is a name identifying this string, and is only
141 argument `name` is a name identifying this string, and is only
157 used for error messages.
142 used for error messages.
158 """
143 """
159
144
160 #print 'Parse string:\n',string # dbg
145 #print 'Parse string:\n',string # dbg
161
146
162 string = string.expandtabs()
147 string = string.expandtabs()
163 # If all lines begin with the same indentation, then strip it.
148 # If all lines begin with the same indentation, then strip it.
164 min_indent = self._min_indent(string)
149 min_indent = self._min_indent(string)
165 if min_indent > 0:
150 if min_indent > 0:
166 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
151 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
167
152
168 output = []
153 output = []
169 charno, lineno = 0, 0
154 charno, lineno = 0, 0
170
155
171 # We make 'all random' tests by adding the '# random' mark to every
156 # We make 'all random' tests by adding the '# random' mark to every
172 # block of output in the test.
157 # block of output in the test.
173 if self._RANDOM_TEST.search(string):
158 if self._RANDOM_TEST.search(string):
174 random_marker = '\n# random'
159 random_marker = '\n# random'
175 else:
160 else:
176 random_marker = ''
161 random_marker = ''
177
162
178 # Whether to convert the input from ipython to python syntax
163 # Whether to convert the input from ipython to python syntax
179 ip2py = False
164 ip2py = False
180 # Find all doctest examples in the string. First, try them as Python
165 # Find all doctest examples in the string. First, try them as Python
181 # examples, then as IPython ones
166 # examples, then as IPython ones
182 terms = list(self._EXAMPLE_RE_PY.finditer(string))
167 terms = list(self._EXAMPLE_RE_PY.finditer(string))
183 if terms:
168 if terms:
184 # Normal Python example
169 # Normal Python example
185 #print '-'*70 # dbg
186 #print 'PyExample, Source:\n',string # dbg
187 #print '-'*70 # dbg
188 Example = doctest.Example
170 Example = doctest.Example
189 else:
171 else:
190 # It's an ipython example. Note that IPExamples are run
172 # It's an ipython example.
191 # in-process, so their syntax must be turned into valid python.
192 # IPExternalExamples are run out-of-process (via pexpect) so they
193 # don't need any filtering (a real ipython will be executing them).
194 terms = list(self._EXAMPLE_RE_IP.finditer(string))
173 terms = list(self._EXAMPLE_RE_IP.finditer(string))
195 if self._EXTERNAL_IP.search(string):
174 Example = IPExample
196 #print '-'*70 # dbg
175 ip2py = True
197 #print 'IPExternalExample, Source:\n',string # dbg
198 #print '-'*70 # dbg
199 Example = IPExternalExample
200 else:
201 #print '-'*70 # dbg
202 #print 'IPExample, Source:\n',string # dbg
203 #print '-'*70 # dbg
204 Example = IPExample
205 ip2py = True
206
176
207 for m in terms:
177 for m in terms:
208 # Add the pre-example text to `output`.
178 # Add the pre-example text to `output`.
209 output.append(string[charno:m.start()])
179 output.append(string[charno:m.start()])
210 # Update lineno (lines before this example)
180 # Update lineno (lines before this example)
211 lineno += string.count('\n', charno, m.start())
181 lineno += string.count('\n', charno, m.start())
212 # Extract info from the regexp match.
182 # Extract info from the regexp match.
213 (source, options, want, exc_msg) = \
183 (source, options, want, exc_msg) = \
214 self._parse_example(m, name, lineno,ip2py)
184 self._parse_example(m, name, lineno,ip2py)
215
185
216 # Append the random-output marker (it defaults to empty in most
186 # Append the random-output marker (it defaults to empty in most
217 # cases, it's only non-empty for 'all-random' tests):
187 # cases, it's only non-empty for 'all-random' tests):
218 want += random_marker
188 want += random_marker
219
189
220 if Example is IPExternalExample:
221 options[doctest.NORMALIZE_WHITESPACE] = True
222 want += '\n'
223
224 # Create an Example, and add it to the list.
190 # Create an Example, and add it to the list.
225 if not self._IS_BLANK_OR_COMMENT(source):
191 if not self._IS_BLANK_OR_COMMENT(source):
226 output.append(Example(source, want, exc_msg,
192 output.append(Example(source, want, exc_msg,
227 lineno=lineno,
193 lineno=lineno,
228 indent=min_indent+len(m.group('indent')),
194 indent=min_indent+len(m.group('indent')),
229 options=options))
195 options=options))
230 # Update lineno (lines inside this example)
196 # Update lineno (lines inside this example)
231 lineno += string.count('\n', m.start(), m.end())
197 lineno += string.count('\n', m.start(), m.end())
232 # Update charno.
198 # Update charno.
233 charno = m.end()
199 charno = m.end()
234 # Add any remaining post-example text to `output`.
200 # Add any remaining post-example text to `output`.
235 output.append(string[charno:])
201 output.append(string[charno:])
236 return output
202 return output
237
203
238 def _parse_example(self, m, name, lineno,ip2py=False):
204 def _parse_example(self, m, name, lineno,ip2py=False):
239 """
205 """
240 Given a regular expression match from `_EXAMPLE_RE` (`m`),
206 Given a regular expression match from `_EXAMPLE_RE` (`m`),
241 return a pair `(source, want)`, where `source` is the matched
207 return a pair `(source, want)`, where `source` is the matched
242 example's source code (with prompts and indentation stripped);
208 example's source code (with prompts and indentation stripped);
243 and `want` is the example's expected output (with indentation
209 and `want` is the example's expected output (with indentation
244 stripped).
210 stripped).
245
211
246 `name` is the string's name, and `lineno` is the line number
212 `name` is the string's name, and `lineno` is the line number
247 where the example starts; both are used for error messages.
213 where the example starts; both are used for error messages.
248
214
249 Optional:
215 Optional:
250 `ip2py`: if true, filter the input via IPython to convert the syntax
216 `ip2py`: if true, filter the input via IPython to convert the syntax
251 into valid python.
217 into valid python.
252 """
218 """
253
219
254 # Get the example's indentation level.
220 # Get the example's indentation level.
255 indent = len(m.group('indent'))
221 indent = len(m.group('indent'))
256
222
257 # Divide source into lines; check that they're properly
223 # Divide source into lines; check that they're properly
258 # indented; and then strip their indentation & prompts.
224 # indented; and then strip their indentation & prompts.
259 source_lines = m.group('source').split('\n')
225 source_lines = m.group('source').split('\n')
260
226
261 # We're using variable-length input prompts
227 # We're using variable-length input prompts
262 ps1 = m.group('ps1')
228 ps1 = m.group('ps1')
263 ps2 = m.group('ps2')
229 ps2 = m.group('ps2')
264 ps1_len = len(ps1)
230 ps1_len = len(ps1)
265
231
266 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
232 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
267 if ps2:
233 if ps2:
268 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
234 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
269
235
270 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
236 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
271
237
272 if ip2py:
238 if ip2py:
273 # Convert source input from IPython into valid Python syntax
239 # Convert source input from IPython into valid Python syntax
274 source = self.ip2py(source)
240 source = self.ip2py(source)
275
241
276 # Divide want into lines; check that it's properly indented; and
242 # Divide want into lines; check that it's properly indented; and
277 # then strip the indentation. Spaces before the last newline should
243 # then strip the indentation. Spaces before the last newline should
278 # be preserved, so plain rstrip() isn't good enough.
244 # be preserved, so plain rstrip() isn't good enough.
279 want = m.group('want')
245 want = m.group('want')
280 want_lines = want.split('\n')
246 want_lines = want.split('\n')
281 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
247 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
282 del want_lines[-1] # forget final newline & spaces after it
248 del want_lines[-1] # forget final newline & spaces after it
283 self._check_prefix(want_lines, ' '*indent, name,
249 self._check_prefix(want_lines, ' '*indent, name,
284 lineno + len(source_lines))
250 lineno + len(source_lines))
285
251
286 # Remove ipython output prompt that might be present in the first line
252 # Remove ipython output prompt that might be present in the first line
287 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
253 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
288
254
289 want = '\n'.join([wl[indent:] for wl in want_lines])
255 want = '\n'.join([wl[indent:] for wl in want_lines])
290
256
291 # If `want` contains a traceback message, then extract it.
257 # If `want` contains a traceback message, then extract it.
292 m = self._EXCEPTION_RE.match(want)
258 m = self._EXCEPTION_RE.match(want)
293 if m:
259 if m:
294 exc_msg = m.group('msg')
260 exc_msg = m.group('msg')
295 else:
261 else:
296 exc_msg = None
262 exc_msg = None
297
263
298 # Extract options from the source.
264 # Extract options from the source.
299 options = self._find_options(source, name, lineno)
265 options = self._find_options(source, name, lineno)
300
266
301 return source, options, want, exc_msg
267 return source, options, want, exc_msg
302
268
303 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
269 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
304 """
270 """
305 Given the lines of a source string (including prompts and
271 Given the lines of a source string (including prompts and
306 leading indentation), check to make sure that every prompt is
272 leading indentation), check to make sure that every prompt is
307 followed by a space character. If any line is not followed by
273 followed by a space character. If any line is not followed by
308 a space character, then raise ValueError.
274 a space character, then raise ValueError.
309
275
310 Note: IPython-modified version which takes the input prompt length as a
276 Note: IPython-modified version which takes the input prompt length as a
311 parameter, so that prompts of variable length can be dealt with.
277 parameter, so that prompts of variable length can be dealt with.
312 """
278 """
313 space_idx = indent+ps1_len
279 space_idx = indent+ps1_len
314 min_len = space_idx+1
280 min_len = space_idx+1
315 for i, line in enumerate(lines):
281 for i, line in enumerate(lines):
316 if len(line) >= min_len and line[space_idx] != ' ':
282 if len(line) >= min_len and line[space_idx] != ' ':
317 raise ValueError('line %r of the docstring for %s '
283 raise ValueError('line %r of the docstring for %s '
318 'lacks blank after %s: %r' %
284 'lacks blank after %s: %r' %
319 (lineno+i+1, name,
285 (lineno+i+1, name,
320 line[indent:space_idx], line))
286 line[indent:space_idx], line))
321
287
322
288
323 SKIP = doctest.register_optionflag('SKIP')
289 SKIP = doctest.register_optionflag('SKIP')
324
290
325
291
326 class IPDocTestRunner(doctest.DocTestRunner,object):
292 class IPDocTestRunner(doctest.DocTestRunner,object):
327 """Test runner that synchronizes the IPython namespace with test globals.
293 """Test runner that synchronizes the IPython namespace with test globals.
328 """
294 """
329
295
330 def run(self, test, compileflags=None, out=None, clear_globs=True):
296 def run(self, test, compileflags=None, out=None, clear_globs=True):
331
332 # Hack: ipython needs access to the execution context of the example,
333 # so that it can propagate user variables loaded by %run into
334 # test.globs. We put them here into our modified %run as a function
335 # attribute. Our new %run will then only make the namespace update
336 # when called (rather than unconditionally updating test.globs here
337 # for all examples, most of which won't be calling %run anyway).
338 #_ip._ipdoctest_test_globs = test.globs
339 #_ip._ipdoctest_test_filename = test.filename
340
341 test.globs.update(_ip.user_ns)
342
343 # Override terminal size to standardise traceback format
297 # Override terminal size to standardise traceback format
344 with modified_env({'COLUMNS': '80', 'LINES': '24'}):
298 with modified_env({'COLUMNS': '80', 'LINES': '24'}):
345 return super(IPDocTestRunner,self).run(test,
299 return super(IPDocTestRunner,self).run(test,
346 compileflags,out,clear_globs)
300 compileflags,out,clear_globs)
@@ -1,33 +1,44 b''
1 """Simple example using doctests.
1 """Simple example using doctests.
2
2
3 This file just contains doctests both using plain python and IPython prompts.
3 This file just contains doctests both using plain python and IPython prompts.
4 All tests should be loaded by nose.
4 All tests should be loaded by Pytest.
5 """
5 """
6
6
7 def pyfunc():
7 def pyfunc():
8 """Some pure python tests...
8 """Some pure python tests...
9
9
10 >>> pyfunc()
10 >>> pyfunc()
11 'pyfunc'
11 'pyfunc'
12
12
13 >>> import os
13 >>> import os
14
14
15 >>> 2+3
15 >>> 2+3
16 5
16 5
17
17
18 >>> for i in range(3):
18 >>> for i in range(3):
19 ... print(i, end=' ')
19 ... print(i, end=' ')
20 ... print(i+1, end=' ')
20 ... print(i+1, end=' ')
21 ...
21 ...
22 0 1 1 2 2 3
22 0 1 1 2 2 3
23 """
23 """
24 return 'pyfunc'
24 return 'pyfunc'
25
25
26
26
27 def ipyfunc2():
27 def ipyfunc():
28 """Some pure python tests...
28 """Some IPython tests...
29
30 In [1]: ipyfunc()
31 Out[1]: 'ipyfunc'
32
33 In [2]: import os
34
35 In [3]: 2+3
36 Out[3]: 5
29
37
30 >>> 1+1
38 In [4]: for i in range(3):
31 2
39 ...: print(i, end=' ')
40 ...: print(i+1, end=' ')
41 ...:
42 Out[4]: 0 1 1 2 2 3
32 """
43 """
33 return 'pyfunc2'
44 return "ipyfunc"
General Comments 0
You need to be logged in to leave comments. Login now