##// END OF EJS Templates
Deprecate inputtransformer since 7.0...
M Bussonnier -
Show More
@@ -1,544 +1,577
1 1 """DEPRECATED: Input transformer classes to support IPython special syntax.
2 2
3 3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
4 4
5 5 This includes the machinery to recognise and transform ``%magic`` commands,
6 6 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
7 7 """
8 8 import abc
9 9 import functools
10 10 import re
11 11 import tokenize
12 import warnings
12 13 from tokenize import untokenize, TokenError
13 14 from io import StringIO
14 15
15 16 from IPython.core.splitinput import LineInfo
16 17 from IPython.utils import tokenutil
17 18
18 19 #-----------------------------------------------------------------------------
19 20 # Globals
20 21 #-----------------------------------------------------------------------------
21 22
22 23 # The escape sequences that define the syntax transformations IPython will
23 24 # apply to user input. These can NOT be just changed here: many regular
24 25 # expressions and other parts of the code may use their hardcoded values, and
25 26 # for all intents and purposes they constitute the 'IPython syntax', so they
26 27 # should be considered fixed.
27 28
28 29 ESC_SHELL = '!' # Send line to underlying system shell
29 30 ESC_SH_CAP = '!!' # Send line to system shell and capture output
30 31 ESC_HELP = '?' # Find information about object
31 32 ESC_HELP2 = '??' # Find extra-detailed information about object
32 33 ESC_MAGIC = '%' # Call magic function
33 34 ESC_MAGIC2 = '%%' # Call cell-magic function
34 35 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
35 36 ESC_QUOTE2 = ';' # Quote all args as a single string, call
36 37 ESC_PAREN = '/' # Call first argument with rest of line as arguments
37 38
38 39 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
39 40 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
40 41 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
41 42
42 43
43 44 class InputTransformer(metaclass=abc.ABCMeta):
44 45 """Abstract base class for line-based input transformers."""
45 46
47 def __init__(self):
48 warnings.warn(
49 "`InputTransformer` has been deprecated since IPython 7.0"
50 " and emit a warnig since IPython 8.31, it"
51 " will be removed in the future",
52 DeprecationWarning,
53 stacklevel=2,
54 )
55
46 56 @abc.abstractmethod
47 57 def push(self, line):
48 58 """Send a line of input to the transformer, returning the transformed
49 59 input or None if the transformer is waiting for more input.
50 60
51 61 Must be overridden by subclasses.
52 62
53 63 Implementations may raise ``SyntaxError`` if the input is invalid. No
54 64 other exceptions may be raised.
55 65 """
56 66 pass
57 67
58 68 @abc.abstractmethod
59 69 def reset(self):
60 70 """Return, transformed any lines that the transformer has accumulated,
61 71 and reset its internal state.
62 72
63 73 Must be overridden by subclasses.
64 74 """
65 75 pass
66 76
67 77 @classmethod
68 78 def wrap(cls, func):
69 79 """Can be used by subclasses as a decorator, to return a factory that
70 80 will allow instantiation with the decorated object.
71 81 """
72 82 @functools.wraps(func)
73 83 def transformer_factory(**kwargs):
74 84 return cls(func, **kwargs) # type: ignore [call-arg]
75 85
76 86 return transformer_factory
77 87
78 88 class StatelessInputTransformer(InputTransformer):
79 89 """Wrapper for a stateless input transformer implemented as a function."""
80 90 def __init__(self, func):
91 super().__init__()
92 warnings.warn(
93 "`StatelessInputTransformer` has been deprecated since IPython 7.0"
94 " and emit a warnig since IPython 8.31, it"
95 " will be removed in the future",
96 DeprecationWarning,
97 stacklevel=2,
98 )
81 99 self.func = func
82 100
83 101 def __repr__(self):
84 102 return "StatelessInputTransformer(func={0!r})".format(self.func)
85 103
86 104 def push(self, line):
87 105 """Send a line of input to the transformer, returning the
88 106 transformed input."""
89 107 return self.func(line)
90 108
91 109 def reset(self):
92 110 """No-op - exists for compatibility."""
93 111 pass
94 112
95 113 class CoroutineInputTransformer(InputTransformer):
96 114 """Wrapper for an input transformer implemented as a coroutine."""
97 115 def __init__(self, coro, **kwargs):
98 116 # Prime it
117 super().__init__()
118 warnings.warn(
119 "`CoroutineInputTransformer` has been deprecated since IPython 7.0"
120 " and emit a warnig since IPython 8.31, it"
121 " will be removed in the future",
122 DeprecationWarning,
123 stacklevel=2,
124 )
99 125 self.coro = coro(**kwargs)
100 126 next(self.coro)
101 127
102 128 def __repr__(self):
103 129 return "CoroutineInputTransformer(coro={0!r})".format(self.coro)
104 130
105 131 def push(self, line):
106 132 """Send a line of input to the transformer, returning the
107 133 transformed input or None if the transformer is waiting for more
108 134 input.
109 135 """
110 136 return self.coro.send(line)
111 137
112 138 def reset(self):
113 139 """Return, transformed any lines that the transformer has
114 140 accumulated, and reset its internal state.
115 141 """
116 142 return self.coro.send(None)
117 143
118 144 class TokenInputTransformer(InputTransformer):
119 145 """Wrapper for a token-based input transformer.
120 146
121 147 func should accept a list of tokens (5-tuples, see tokenize docs), and
122 148 return an iterable which can be passed to tokenize.untokenize().
123 149 """
124 150 def __init__(self, func):
151 warnings.warn(
152 "`CoroutineInputTransformer` has been deprecated since IPython 7.0"
153 " and emit a warnig since IPython 8.31, it"
154 " will be removed in the future",
155 DeprecationWarning,
156 stacklevel=2,
157 )
125 158 self.func = func
126 159 self.buf = []
127 160 self.reset_tokenizer()
128 161
129 162 def reset_tokenizer(self):
130 163 it = iter(self.buf)
131 164 self.tokenizer = tokenutil.generate_tokens_catch_errors(it.__next__)
132 165
133 166 def push(self, line):
134 167 self.buf.append(line + '\n')
135 168 if all(l.isspace() for l in self.buf):
136 169 return self.reset()
137 170
138 171 tokens = []
139 172 stop_at_NL = False
140 173 try:
141 174 for intok in self.tokenizer:
142 175 tokens.append(intok)
143 176 t = intok[0]
144 177 if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL):
145 178 # Stop before we try to pull a line we don't have yet
146 179 break
147 180 elif t == tokenize.ERRORTOKEN:
148 181 stop_at_NL = True
149 182 except TokenError:
150 183 # Multi-line statement - stop and try again with the next line
151 184 self.reset_tokenizer()
152 185 return None
153 186
154 187 return self.output(tokens)
155 188
156 189 def output(self, tokens):
157 190 self.buf.clear()
158 191 self.reset_tokenizer()
159 192 return untokenize(self.func(tokens)).rstrip('\n')
160 193
161 194 def reset(self):
162 195 l = ''.join(self.buf)
163 196 self.buf.clear()
164 197 self.reset_tokenizer()
165 198 if l:
166 199 return l.rstrip('\n')
167 200
168 201 class assemble_python_lines(TokenInputTransformer):
169 202 def __init__(self):
170 super(assemble_python_lines, self).__init__(None)
203 super().__init__(None)
171 204
172 205 def output(self, tokens):
173 206 return self.reset()
174 207
175 208 @CoroutineInputTransformer.wrap
176 209 def assemble_logical_lines():
177 210 r"""Join lines following explicit line continuations (\)"""
178 211 line = ''
179 212 while True:
180 213 line = (yield line)
181 214 if not line or line.isspace():
182 215 continue
183 216
184 217 parts = []
185 218 while line is not None:
186 219 if line.endswith('\\') and (not has_comment(line)):
187 220 parts.append(line[:-1])
188 221 line = (yield None) # Get another line
189 222 else:
190 223 parts.append(line)
191 224 break
192 225
193 226 # Output
194 227 line = ''.join(parts)
195 228
196 229 # Utilities
197 230 def _make_help_call(target: str, esc: str, lspace: str) -> str:
198 231 """Prepares a pinfo(2)/psearch call from a target name and the escape
199 232 (i.e. ? or ??)"""
200 233 method = 'pinfo2' if esc == '??' \
201 234 else 'psearch' if '*' in target \
202 235 else 'pinfo'
203 236 arg = " ".join([method, target])
204 237 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
205 238 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
206 239 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
207 240 return "%sget_ipython().run_line_magic(%r, %r)" % (
208 241 lspace,
209 242 t_magic_name,
210 243 t_magic_arg_s,
211 244 )
212 245
213 246
214 247 # These define the transformations for the different escape characters.
215 248 def _tr_system(line_info: LineInfo):
216 249 "Translate lines escaped with: !"
217 250 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
218 251 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
219 252
220 253
221 254 def _tr_system2(line_info: LineInfo):
222 255 "Translate lines escaped with: !!"
223 256 cmd = line_info.line.lstrip()[2:]
224 257 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
225 258
226 259
227 260 def _tr_help(line_info: LineInfo):
228 261 "Translate lines escaped with: ?/??"
229 262 # A naked help line should just fire the intro help screen
230 263 if not line_info.line[1:]:
231 264 return 'get_ipython().show_usage()'
232 265
233 266 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
234 267
235 268
236 269 def _tr_magic(line_info: LineInfo):
237 270 "Translate lines escaped with: %"
238 271 tpl = '%sget_ipython().run_line_magic(%r, %r)'
239 272 if line_info.line.startswith(ESC_MAGIC2):
240 273 return line_info.line
241 274 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
242 275 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
243 276 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
244 277 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
245 278 return tpl % (line_info.pre, t_magic_name, t_magic_arg_s)
246 279
247 280
248 281 def _tr_quote(line_info: LineInfo):
249 282 "Translate lines escaped with: ,"
250 283 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
251 284 '", "'.join(line_info.the_rest.split()) )
252 285
253 286
254 287 def _tr_quote2(line_info: LineInfo):
255 288 "Translate lines escaped with: ;"
256 289 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
257 290 line_info.the_rest)
258 291
259 292
260 293 def _tr_paren(line_info: LineInfo):
261 294 "Translate lines escaped with: /"
262 295 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
263 296 ", ".join(line_info.the_rest.split()))
264 297
265 298 tr = { ESC_SHELL : _tr_system,
266 299 ESC_SH_CAP : _tr_system2,
267 300 ESC_HELP : _tr_help,
268 301 ESC_HELP2 : _tr_help,
269 302 ESC_MAGIC : _tr_magic,
270 303 ESC_QUOTE : _tr_quote,
271 304 ESC_QUOTE2 : _tr_quote2,
272 305 ESC_PAREN : _tr_paren }
273 306
274 307 @StatelessInputTransformer.wrap
275 308 def escaped_commands(line: str):
276 309 """Transform escaped commands - %magic, !system, ?help + various autocalls."""
277 310 if not line or line.isspace():
278 311 return line
279 312 lineinf = LineInfo(line)
280 313 if lineinf.esc not in tr:
281 314 return line
282 315
283 316 return tr[lineinf.esc](lineinf)
284 317
285 318 _initial_space_re = re.compile(r'\s*')
286 319
287 320 _help_end_re = re.compile(r"""(%{0,2}
288 321 (?!\d)[\w*]+ # Variable name
289 322 (\.(?!\d)[\w*]+)* # .etc.etc
290 323 )
291 324 (\?\??)$ # ? or ??
292 325 """,
293 326 re.VERBOSE)
294 327
295 328 # Extra pseudotokens for multiline strings and data structures
296 329 _MULTILINE_STRING = object()
297 330 _MULTILINE_STRUCTURE = object()
298 331
299 332 def _line_tokens(line):
300 333 """Helper for has_comment and ends_in_comment_or_string."""
301 334 readline = StringIO(line).readline
302 335 toktypes = set()
303 336 try:
304 337 for t in tokenutil.generate_tokens_catch_errors(readline):
305 338 toktypes.add(t[0])
306 339 except TokenError as e:
307 340 # There are only two cases where a TokenError is raised.
308 341 if 'multi-line string' in e.args[0]:
309 342 toktypes.add(_MULTILINE_STRING)
310 343 else:
311 344 toktypes.add(_MULTILINE_STRUCTURE)
312 345 return toktypes
313 346
314 347 def has_comment(src):
315 348 """Indicate whether an input line has (i.e. ends in, or is) a comment.
316 349
317 350 This uses tokenize, so it can distinguish comments from # inside strings.
318 351
319 352 Parameters
320 353 ----------
321 354 src : string
322 355 A single line input string.
323 356
324 357 Returns
325 358 -------
326 359 comment : bool
327 360 True if source has a comment.
328 361 """
329 362 return (tokenize.COMMENT in _line_tokens(src))
330 363
331 364 def ends_in_comment_or_string(src):
332 365 """Indicates whether or not an input line ends in a comment or within
333 366 a multiline string.
334 367
335 368 Parameters
336 369 ----------
337 370 src : string
338 371 A single line input string.
339 372
340 373 Returns
341 374 -------
342 375 comment : bool
343 376 True if source ends in a comment or multiline string.
344 377 """
345 378 toktypes = _line_tokens(src)
346 379 return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
347 380
348 381
349 382 @StatelessInputTransformer.wrap
350 383 def help_end(line: str):
351 384 """Translate lines with ?/?? at the end"""
352 385 m = _help_end_re.search(line)
353 386 if m is None or ends_in_comment_or_string(line):
354 387 return line
355 388 target = m.group(1)
356 389 esc = m.group(3)
357 390 match = _initial_space_re.match(line)
358 391 assert match is not None
359 392 lspace = match.group(0)
360 393
361 394 return _make_help_call(target, esc, lspace)
362 395
363 396
364 397 @CoroutineInputTransformer.wrap
365 398 def cellmagic(end_on_blank_line: bool = False):
366 399 """Captures & transforms cell magics.
367 400
368 401 After a cell magic is started, this stores up any lines it gets until it is
369 402 reset (sent None).
370 403 """
371 404 tpl = 'get_ipython().run_cell_magic(%r, %r, %r)'
372 405 cellmagic_help_re = re.compile(r'%%\w+\?')
373 406 line = ''
374 407 while True:
375 408 line = (yield line)
376 409 # consume leading empty lines
377 410 while not line:
378 411 line = (yield line)
379 412
380 413 if not line.startswith(ESC_MAGIC2):
381 414 # This isn't a cell magic, idle waiting for reset then start over
382 415 while line is not None:
383 416 line = (yield line)
384 417 continue
385 418
386 419 if cellmagic_help_re.match(line):
387 420 # This case will be handled by help_end
388 421 continue
389 422
390 423 first = line
391 424 body = []
392 425 line = (yield None)
393 426 while (line is not None) and \
394 427 ((line.strip() != '') or not end_on_blank_line):
395 428 body.append(line)
396 429 line = (yield None)
397 430
398 431 # Output
399 432 magic_name, _, first = first.partition(' ')
400 433 magic_name = magic_name.lstrip(ESC_MAGIC2)
401 434 line = tpl % (magic_name, first, u'\n'.join(body))
402 435
403 436
404 437 def _strip_prompts(prompt_re, initial_re=None, turnoff_re=None):
405 438 """Remove matching input prompts from a block of input.
406 439
407 440 Parameters
408 441 ----------
409 442 prompt_re : regular expression
410 443 A regular expression matching any input prompt (including continuation)
411 444 initial_re : regular expression, optional
412 445 A regular expression matching only the initial prompt, but not continuation.
413 446 If no initial expression is given, prompt_re will be used everywhere.
414 447 Used mainly for plain Python prompts, where the continuation prompt
415 448 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
416 449
417 450 Notes
418 451 -----
419 452 If `initial_re` and `prompt_re differ`,
420 453 only `initial_re` will be tested against the first line.
421 454 If any prompt is found on the first two lines,
422 455 prompts will be stripped from the rest of the block.
423 456 """
424 457 if initial_re is None:
425 458 initial_re = prompt_re
426 459 line = ''
427 460 while True:
428 461 line = (yield line)
429 462
430 463 # First line of cell
431 464 if line is None:
432 465 continue
433 466 out, n1 = initial_re.subn('', line, count=1)
434 467 if turnoff_re and not n1:
435 468 if turnoff_re.match(line):
436 469 # We're in e.g. a cell magic; disable this transformer for
437 470 # the rest of the cell.
438 471 while line is not None:
439 472 line = (yield line)
440 473 continue
441 474
442 475 line = (yield out)
443 476
444 477 if line is None:
445 478 continue
446 479 # check for any prompt on the second line of the cell,
447 480 # because people often copy from just after the first prompt,
448 481 # so we might not see it in the first line.
449 482 out, n2 = prompt_re.subn('', line, count=1)
450 483 line = (yield out)
451 484
452 485 if n1 or n2:
453 486 # Found a prompt in the first two lines - check for it in
454 487 # the rest of the cell as well.
455 488 while line is not None:
456 489 line = (yield prompt_re.sub('', line, count=1))
457 490
458 491 else:
459 492 # Prompts not in input - wait for reset
460 493 while line is not None:
461 494 line = (yield line)
462 495
463 496 @CoroutineInputTransformer.wrap
464 497 def classic_prompt():
465 498 """Strip the >>>/... prompts of the Python interactive shell."""
466 499 # FIXME: non-capturing version (?:...) usable?
467 500 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
468 501 initial_re = re.compile(r'^>>>( |$)')
469 502 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
470 503 turnoff_re = re.compile(r'^[%!]')
471 504 return _strip_prompts(prompt_re, initial_re, turnoff_re)
472 505
473 506 @CoroutineInputTransformer.wrap
474 507 def ipy_prompt():
475 508 """Strip IPython's In [1]:/...: prompts."""
476 509 # FIXME: non-capturing version (?:...) usable?
477 510 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
478 511 # Disable prompt stripping inside cell magics
479 512 turnoff_re = re.compile(r'^%%')
480 513 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
481 514
482 515
483 516 @CoroutineInputTransformer.wrap
484 517 def leading_indent():
485 518 """Remove leading indentation.
486 519
487 520 If the first line starts with a spaces or tabs, the same whitespace will be
488 521 removed from each following line until it is reset.
489 522 """
490 523 space_re = re.compile(r'^[ \t]+')
491 524 line = ''
492 525 while True:
493 526 line = (yield line)
494 527
495 528 if line is None:
496 529 continue
497 530
498 531 m = space_re.match(line)
499 532 if m:
500 533 space = m.group(0)
501 534 while line is not None:
502 535 if line.startswith(space):
503 536 line = line[len(space):]
504 537 line = (yield line)
505 538 else:
506 539 # No leading spaces - wait for reset
507 540 while line is not None:
508 541 line = (yield line)
509 542
510 543
511 544 _assign_pat = \
512 545 r'''(?P<lhs>(\s*)
513 546 ([\w\.]+) # Initial identifier
514 547 (\s*,\s*
515 548 \*?[\w\.]+)* # Further identifiers for unpacking
516 549 \s*?,? # Trailing comma
517 550 )
518 551 \s*=\s*
519 552 '''
520 553
521 554 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
522 555 assign_system_template = '%s = get_ipython().getoutput(%r)'
523 556 @StatelessInputTransformer.wrap
524 557 def assign_from_system(line):
525 558 """Transform assignment from system commands (e.g. files = !ls)"""
526 559 m = assign_system_re.match(line)
527 560 if m is None:
528 561 return line
529 562
530 563 return assign_system_template % m.group('lhs', 'cmd')
531 564
532 565 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
533 566 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
534 567 @StatelessInputTransformer.wrap
535 568 def assign_from_magic(line):
536 569 """Transform assignment from magic commands (e.g. a = %who_ls)"""
537 570 m = assign_magic_re.match(line)
538 571 if m is None:
539 572 return line
540 573 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
541 574 m_lhs, m_cmd = m.group('lhs', 'cmd')
542 575 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
543 576 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
544 577 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
@@ -1,643 +1,637
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for the inputsplitter module."""
3 3
4 4
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8 import unittest
9 9 import pytest
10 10 import sys
11 11
12 12 with pytest.warns(DeprecationWarning, match="inputsplitter"):
13 13 from IPython.core import inputsplitter as isp
14 14 from IPython.core.inputtransformer import InputTransformer
15 15 from IPython.core.tests.test_inputtransformer import syntax, syntax_ml
16 16 from IPython.testing import tools as tt
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Semi-complete examples (also used as tests)
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # Note: at the bottom, there's a slightly more complete version of this that
23 23 # can be useful during development of code here.
24 24
25 25 def mini_interactive_loop(input_func):
26 26 """Minimal example of the logic of an interactive interpreter loop.
27 27
28 28 This serves as an example, and it is used by the test system with a fake
29 29 raw_input that simulates interactive input."""
30 30
31 31 from IPython.core.inputsplitter import InputSplitter
32 32
33 33 isp = InputSplitter()
34 34 # In practice, this input loop would be wrapped in an outside loop to read
35 35 # input indefinitely, until some exit/quit command was issued. Here we
36 36 # only illustrate the basic inner loop.
37 37 while isp.push_accepts_more():
38 38 indent = ' '*isp.get_indent_spaces()
39 39 prompt = '>>> ' + indent
40 40 line = indent + input_func(prompt)
41 41 isp.push(line)
42 42
43 43 # Here we just return input so we can use it in a test suite, but a real
44 44 # interpreter would instead send it for execution somewhere.
45 45 src = isp.source_reset()
46 46 # print('Input source was:\n', src) # dbg
47 47 return src
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Test utilities, just for local use
51 51 #-----------------------------------------------------------------------------
52 52
53 53
54 54 def pseudo_input(lines):
55 55 """Return a function that acts like raw_input but feeds the input list."""
56 56 ilines = iter(lines)
57 57 def raw_in(prompt):
58 58 try:
59 59 return next(ilines)
60 60 except StopIteration:
61 61 return ''
62 62 return raw_in
63 63
64 64 #-----------------------------------------------------------------------------
65 65 # Tests
66 66 #-----------------------------------------------------------------------------
67 67 def test_spaces():
68 68 tests = [('', 0),
69 69 (' ', 1),
70 70 ('\n', 0),
71 71 (' \n', 1),
72 72 ('x', 0),
73 73 (' x', 1),
74 74 (' x',2),
75 75 (' x',4),
76 76 # Note: tabs are counted as a single whitespace!
77 77 ('\tx', 1),
78 78 ('\t x', 2),
79 79 ]
80 80 with pytest.warns(PendingDeprecationWarning):
81 81 tt.check_pairs(isp.num_ini_spaces, tests)
82 82
83 83
84 84 def test_remove_comments():
85 85 tests = [('text', 'text'),
86 86 ('text # comment', 'text '),
87 87 ('text # comment\n', 'text \n'),
88 88 ('text # comment \n', 'text \n'),
89 89 ('line # c \nline\n','line \nline\n'),
90 90 ('line # c \nline#c2 \nline\nline #c\n\n',
91 91 'line \nline\nline\nline \n\n'),
92 92 ]
93 93 tt.check_pairs(isp.remove_comments, tests)
94 94
95 95
96 96 def test_get_input_encoding():
97 97 encoding = isp.get_input_encoding()
98 98 assert isinstance(encoding, str)
99 99 # simple-minded check that at least encoding a simple string works with the
100 100 # encoding we got.
101 101 assert "test".encode(encoding) == b"test"
102 102
103 103
104 104 class NoInputEncodingTestCase(unittest.TestCase):
105 105 def setUp(self):
106 106 self.old_stdin = sys.stdin
107 107 class X: pass
108 108 fake_stdin = X()
109 109 sys.stdin = fake_stdin
110 110
111 111 def test(self):
112 112 # Verify that if sys.stdin has no 'encoding' attribute we do the right
113 113 # thing
114 114 enc = isp.get_input_encoding()
115 115 self.assertEqual(enc, 'ascii')
116 116
117 117 def tearDown(self):
118 118 sys.stdin = self.old_stdin
119 119
120 120
121 121 class InputSplitterTestCase(unittest.TestCase):
122 122 def setUp(self):
123 123 self.isp = isp.InputSplitter()
124 124
125 125 def test_reset(self):
126 126 isp = self.isp
127 127 isp.push('x=1')
128 128 isp.reset()
129 129 self.assertEqual(isp._buffer, [])
130 130 self.assertEqual(isp.get_indent_spaces(), 0)
131 131 self.assertEqual(isp.source, '')
132 132 self.assertEqual(isp.code, None)
133 133 self.assertEqual(isp._is_complete, False)
134 134
135 135 def test_source(self):
136 136 self.isp._store('1')
137 137 self.isp._store('2')
138 138 self.assertEqual(self.isp.source, '1\n2\n')
139 139 self.assertEqual(len(self.isp._buffer)>0, True)
140 140 self.assertEqual(self.isp.source_reset(), '1\n2\n')
141 141 self.assertEqual(self.isp._buffer, [])
142 142 self.assertEqual(self.isp.source, '')
143 143
144 144 def test_indent(self):
145 145 isp = self.isp # shorthand
146 146 isp.push('x=1')
147 147 self.assertEqual(isp.get_indent_spaces(), 0)
148 148 isp.push('if 1:\n x=1')
149 149 self.assertEqual(isp.get_indent_spaces(), 4)
150 150 isp.push('y=2\n')
151 151 self.assertEqual(isp.get_indent_spaces(), 0)
152 152
153 153 def test_indent2(self):
154 154 isp = self.isp
155 155 isp.push('if 1:')
156 156 self.assertEqual(isp.get_indent_spaces(), 4)
157 157 isp.push(' x=1')
158 158 self.assertEqual(isp.get_indent_spaces(), 4)
159 159 # Blank lines shouldn't change the indent level
160 160 isp.push(' '*2)
161 161 self.assertEqual(isp.get_indent_spaces(), 4)
162 162
163 163 def test_indent3(self):
164 164 isp = self.isp
165 165 # When a multiline statement contains parens or multiline strings, we
166 166 # shouldn't get confused.
167 167 isp.push("if 1:")
168 168 isp.push(" x = (1+\n 2)")
169 169 self.assertEqual(isp.get_indent_spaces(), 4)
170 170
171 171 def test_indent4(self):
172 172 isp = self.isp
173 173 # whitespace after ':' should not screw up indent level
174 174 isp.push('if 1: \n x=1')
175 175 self.assertEqual(isp.get_indent_spaces(), 4)
176 176 isp.push('y=2\n')
177 177 self.assertEqual(isp.get_indent_spaces(), 0)
178 178 isp.push('if 1:\t\n x=1')
179 179 self.assertEqual(isp.get_indent_spaces(), 4)
180 180 isp.push('y=2\n')
181 181 self.assertEqual(isp.get_indent_spaces(), 0)
182 182
183 183 def test_dedent_pass(self):
184 184 isp = self.isp # shorthand
185 185 # should NOT cause dedent
186 186 isp.push('if 1:\n passes = 5')
187 187 self.assertEqual(isp.get_indent_spaces(), 4)
188 188 isp.push('if 1:\n pass')
189 189 self.assertEqual(isp.get_indent_spaces(), 0)
190 190 isp.push('if 1:\n pass ')
191 191 self.assertEqual(isp.get_indent_spaces(), 0)
192 192
193 193 def test_dedent_break(self):
194 194 isp = self.isp # shorthand
195 195 # should NOT cause dedent
196 196 isp.push('while 1:\n breaks = 5')
197 197 self.assertEqual(isp.get_indent_spaces(), 4)
198 198 isp.push('while 1:\n break')
199 199 self.assertEqual(isp.get_indent_spaces(), 0)
200 200 isp.push('while 1:\n break ')
201 201 self.assertEqual(isp.get_indent_spaces(), 0)
202 202
203 203 def test_dedent_continue(self):
204 204 isp = self.isp # shorthand
205 205 # should NOT cause dedent
206 206 isp.push('while 1:\n continues = 5')
207 207 self.assertEqual(isp.get_indent_spaces(), 4)
208 208 isp.push('while 1:\n continue')
209 209 self.assertEqual(isp.get_indent_spaces(), 0)
210 210 isp.push('while 1:\n continue ')
211 211 self.assertEqual(isp.get_indent_spaces(), 0)
212 212
213 213 def test_dedent_raise(self):
214 214 isp = self.isp # shorthand
215 215 # should NOT cause dedent
216 216 isp.push('if 1:\n raised = 4')
217 217 self.assertEqual(isp.get_indent_spaces(), 4)
218 218 isp.push('if 1:\n raise TypeError()')
219 219 self.assertEqual(isp.get_indent_spaces(), 0)
220 220 isp.push('if 1:\n raise')
221 221 self.assertEqual(isp.get_indent_spaces(), 0)
222 222 isp.push('if 1:\n raise ')
223 223 self.assertEqual(isp.get_indent_spaces(), 0)
224 224
225 225 def test_dedent_return(self):
226 226 isp = self.isp # shorthand
227 227 # should NOT cause dedent
228 228 isp.push('if 1:\n returning = 4')
229 229 self.assertEqual(isp.get_indent_spaces(), 4)
230 230 isp.push('if 1:\n return 5 + 493')
231 231 self.assertEqual(isp.get_indent_spaces(), 0)
232 232 isp.push('if 1:\n return')
233 233 self.assertEqual(isp.get_indent_spaces(), 0)
234 234 isp.push('if 1:\n return ')
235 235 self.assertEqual(isp.get_indent_spaces(), 0)
236 236 isp.push('if 1:\n return(0)')
237 237 self.assertEqual(isp.get_indent_spaces(), 0)
238 238
239 239 def test_push(self):
240 240 isp = self.isp
241 241 self.assertEqual(isp.push('x=1'), True)
242 242
243 243 def test_push2(self):
244 244 isp = self.isp
245 245 self.assertEqual(isp.push('if 1:'), False)
246 246 for line in [' x=1', '# a comment', ' y=2']:
247 247 print(line)
248 248 self.assertEqual(isp.push(line), True)
249 249
250 250 def test_push3(self):
251 251 isp = self.isp
252 252 isp.push('if True:')
253 253 isp.push(' a = 1')
254 254 self.assertEqual(isp.push('b = [1,'), False)
255 255
256 256 def test_push_accepts_more(self):
257 257 isp = self.isp
258 258 isp.push('x=1')
259 259 self.assertEqual(isp.push_accepts_more(), False)
260 260
261 261 def test_push_accepts_more2(self):
262 262 isp = self.isp
263 263 isp.push('if 1:')
264 264 self.assertEqual(isp.push_accepts_more(), True)
265 265 isp.push(' x=1')
266 266 self.assertEqual(isp.push_accepts_more(), True)
267 267 isp.push('')
268 268 self.assertEqual(isp.push_accepts_more(), False)
269 269
270 270 def test_push_accepts_more3(self):
271 271 isp = self.isp
272 272 isp.push("x = (2+\n3)")
273 273 self.assertEqual(isp.push_accepts_more(), False)
274 274
275 275 def test_push_accepts_more4(self):
276 276 isp = self.isp
277 277 # When a multiline statement contains parens or multiline strings, we
278 278 # shouldn't get confused.
279 279 # FIXME: we should be able to better handle de-dents in statements like
280 280 # multiline strings and multiline expressions (continued with \ or
281 281 # parens). Right now we aren't handling the indentation tracking quite
282 282 # correctly with this, though in practice it may not be too much of a
283 283 # problem. We'll need to see.
284 284 isp.push("if 1:")
285 285 isp.push(" x = (2+")
286 286 isp.push(" 3)")
287 287 self.assertEqual(isp.push_accepts_more(), True)
288 288 isp.push(" y = 3")
289 289 self.assertEqual(isp.push_accepts_more(), True)
290 290 isp.push('')
291 291 self.assertEqual(isp.push_accepts_more(), False)
292 292
293 293 def test_push_accepts_more5(self):
294 294 isp = self.isp
295 295 isp.push('try:')
296 296 isp.push(' a = 5')
297 297 isp.push('except:')
298 298 isp.push(' raise')
299 299 # We want to be able to add an else: block at this point, so it should
300 300 # wait for a blank line.
301 301 self.assertEqual(isp.push_accepts_more(), True)
302 302
303 303 def test_continuation(self):
304 304 isp = self.isp
305 305 isp.push("import os, \\")
306 306 self.assertEqual(isp.push_accepts_more(), True)
307 307 isp.push("sys")
308 308 self.assertEqual(isp.push_accepts_more(), False)
309 309
310 310 def test_syntax_error(self):
311 311 isp = self.isp
312 312 # Syntax errors immediately produce a 'ready' block, so the invalid
313 313 # Python can be sent to the kernel for evaluation with possible ipython
314 314 # special-syntax conversion.
315 315 isp.push('run foo')
316 316 self.assertEqual(isp.push_accepts_more(), False)
317 317
318 318 def test_unicode(self):
319 319 self.isp.push(u"Pérez")
320 320 self.isp.push(u'\xc3\xa9')
321 321 self.isp.push(u"u'\xc3\xa9'")
322 322
323 @pytest.mark.xfail(
324 reason="Bug in python 3.9.8 – bpo 45738",
325 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
326 raises=SystemError,
327 strict=True,
328 )
329 323 def test_line_continuation(self):
330 324 """ Test issue #2108."""
331 325 isp = self.isp
332 326 # A blank line after a line continuation should not accept more
333 327 isp.push("1 \\\n\n")
334 328 self.assertEqual(isp.push_accepts_more(), False)
335 329 # Whitespace after a \ is a SyntaxError. The only way to test that
336 330 # here is to test that push doesn't accept more (as with
337 331 # test_syntax_error() above).
338 332 isp.push(r"1 \ ")
339 333 self.assertEqual(isp.push_accepts_more(), False)
340 334 # Even if the line is continuable (c.f. the regular Python
341 335 # interpreter)
342 336 isp.push(r"(1 \ ")
343 337 self.assertEqual(isp.push_accepts_more(), False)
344 338
345 339 def test_check_complete(self):
346 340 isp = self.isp
347 341 self.assertEqual(isp.check_complete("a = 1"), ('complete', None))
348 342 self.assertEqual(isp.check_complete("for a in range(5):"), ('incomplete', 4))
349 343 self.assertEqual(isp.check_complete("raise = 2"), ('invalid', None))
350 344 self.assertEqual(isp.check_complete("a = [1,\n2,"), ('incomplete', 0))
351 345 self.assertEqual(isp.check_complete("def a():\n x=1\n global x"), ('invalid', None))
352 346
353 347 class InteractiveLoopTestCase(unittest.TestCase):
354 348 """Tests for an interactive loop like a python shell.
355 349 """
356 350 def check_ns(self, lines, ns):
357 351 """Validate that the given input lines produce the resulting namespace.
358 352
359 353 Note: the input lines are given exactly as they would be typed in an
360 354 auto-indenting environment, as mini_interactive_loop above already does
361 355 auto-indenting and prepends spaces to the input.
362 356 """
363 357 src = mini_interactive_loop(pseudo_input(lines))
364 358 test_ns = {}
365 359 exec(src, test_ns)
366 360 # We can't check that the provided ns is identical to the test_ns,
367 361 # because Python fills test_ns with extra keys (copyright, etc). But
368 362 # we can check that the given dict is *contained* in test_ns
369 363 for k,v in ns.items():
370 364 self.assertEqual(test_ns[k], v)
371 365
372 366 def test_simple(self):
373 367 self.check_ns(['x=1'], dict(x=1))
374 368
375 369 def test_simple2(self):
376 370 self.check_ns(['if 1:', 'x=2'], dict(x=2))
377 371
378 372 def test_xy(self):
379 373 self.check_ns(['x=1; y=2'], dict(x=1, y=2))
380 374
381 375 def test_abc(self):
382 376 self.check_ns(['if 1:','a=1','b=2','c=3'], dict(a=1, b=2, c=3))
383 377
384 378 def test_multi(self):
385 379 self.check_ns(['x =(1+','1+','2)'], dict(x=4))
386 380
387 381
388 382 class IPythonInputTestCase(InputSplitterTestCase):
389 383 """By just creating a new class whose .isp is a different instance, we
390 384 re-run the same test battery on the new input splitter.
391 385
392 386 In addition, this runs the tests over the syntax and syntax_ml dicts that
393 387 were tested by individual functions, as part of the OO interface.
394 388
395 389 It also makes some checks on the raw buffer storage.
396 390 """
397 391
398 392 def setUp(self):
399 393 self.isp = isp.IPythonInputSplitter()
400 394
401 395 def test_syntax(self):
402 396 """Call all single-line syntax tests from the main object"""
403 397 isp = self.isp
404 398 for example in syntax.values():
405 399 for raw, out_t in example:
406 400 if raw.startswith(' '):
407 401 continue
408 402
409 403 isp.push(raw+'\n')
410 404 out_raw = isp.source_raw
411 405 out = isp.source_reset()
412 406 self.assertEqual(out.rstrip(), out_t,
413 407 tt.pair_fail_msg.format("inputsplitter",raw, out_t, out))
414 408 self.assertEqual(out_raw.rstrip(), raw.rstrip())
415 409
416 410 def test_syntax_multiline(self):
417 411 isp = self.isp
418 412 for example in syntax_ml.values():
419 413 for line_pairs in example:
420 414 out_t_parts = []
421 415 raw_parts = []
422 416 for lraw, out_t_part in line_pairs:
423 417 if out_t_part is not None:
424 418 out_t_parts.append(out_t_part)
425 419
426 420 if lraw is not None:
427 421 isp.push(lraw)
428 422 raw_parts.append(lraw)
429 423
430 424 out_raw = isp.source_raw
431 425 out = isp.source_reset()
432 426 out_t = '\n'.join(out_t_parts).rstrip()
433 427 raw = '\n'.join(raw_parts).rstrip()
434 428 self.assertEqual(out.rstrip(), out_t)
435 429 self.assertEqual(out_raw.rstrip(), raw)
436 430
437 431 def test_syntax_multiline_cell(self):
438 432 isp = self.isp
439 433 for example in syntax_ml.values():
440 434
441 435 out_t_parts = []
442 436 for line_pairs in example:
443 437 raw = '\n'.join(r for r, _ in line_pairs if r is not None)
444 438 out_t = '\n'.join(t for _,t in line_pairs if t is not None)
445 439 out = isp.transform_cell(raw)
446 440 # Match ignoring trailing whitespace
447 441 self.assertEqual(out.rstrip(), out_t.rstrip())
448 442
449 443 def test_cellmagic_preempt(self):
450 444 isp = self.isp
451 445 for raw, name, line, cell in [
452 446 ("%%cellm a\nIn[1]:", u'cellm', u'a', u'In[1]:'),
453 447 ("%%cellm \nline\n>>> hi", u'cellm', u'', u'line\n>>> hi'),
454 448 (">>> %%cellm \nline\n>>> hi", u'cellm', u'', u'line\nhi'),
455 449 ("%%cellm \n>>> hi", u'cellm', u'', u'>>> hi'),
456 450 ("%%cellm \nline1\nline2", u'cellm', u'', u'line1\nline2'),
457 451 ("%%cellm \nline1\\\\\nline2", u'cellm', u'', u'line1\\\\\nline2'),
458 452 ]:
459 453 expected = "get_ipython().run_cell_magic(%r, %r, %r)" % (
460 454 name, line, cell
461 455 )
462 456 out = isp.transform_cell(raw)
463 457 self.assertEqual(out.rstrip(), expected.rstrip())
464 458
465 459 def test_multiline_passthrough(self):
466 460 isp = self.isp
467 461 class CommentTransformer(InputTransformer):
468 462 def __init__(self):
469 463 self._lines = []
470 464
471 465 def push(self, line):
472 466 self._lines.append(line + '#')
473 467
474 468 def reset(self):
475 469 text = '\n'.join(self._lines)
476 470 self._lines = []
477 471 return text
478 472
479 473 isp.physical_line_transforms.insert(0, CommentTransformer())
480 474
481 475 for raw, expected in [
482 476 ("a=5", "a=5#"),
483 477 ("%ls foo", "get_ipython().run_line_magic(%r, %r)" % (u'ls', u'foo#')),
484 478 ("!ls foo\n%ls bar", "get_ipython().system(%r)\nget_ipython().run_line_magic(%r, %r)" % (
485 479 u'ls foo#', u'ls', u'bar#'
486 480 )),
487 481 ("1\n2\n3\n%ls foo\n4\n5", "1#\n2#\n3#\nget_ipython().run_line_magic(%r, %r)\n4#\n5#" % (u'ls', u'foo#')),
488 482 ]:
489 483 out = isp.transform_cell(raw)
490 484 self.assertEqual(out.rstrip(), expected.rstrip())
491 485
492 486 #-----------------------------------------------------------------------------
493 487 # Main - use as a script, mostly for developer experiments
494 488 #-----------------------------------------------------------------------------
495 489
496 490 if __name__ == '__main__':
497 491 # A simple demo for interactive experimentation. This code will not get
498 492 # picked up by any test suite.
499 493 from IPython.core.inputsplitter import IPythonInputSplitter
500 494
501 495 # configure here the syntax to use, prompt and whether to autoindent
502 496 #isp, start_prompt = InputSplitter(), '>>> '
503 497 isp, start_prompt = IPythonInputSplitter(), 'In> '
504 498
505 499 autoindent = True
506 500 #autoindent = False
507 501
508 502 try:
509 503 while True:
510 504 prompt = start_prompt
511 505 while isp.push_accepts_more():
512 506 indent = ' '*isp.get_indent_spaces()
513 507 if autoindent:
514 508 line = indent + input(prompt+indent)
515 509 else:
516 510 line = input(prompt)
517 511 isp.push(line)
518 512 prompt = '... '
519 513
520 514 # Here we just return input so we can use it in a test suite, but a
521 515 # real interpreter would instead send it for execution somewhere.
522 516 #src = isp.source; raise EOFError # dbg
523 517 raw = isp.source_raw
524 518 src = isp.source_reset()
525 519 print('Input source was:\n', src)
526 520 print('Raw source was:\n', raw)
527 521 except EOFError:
528 522 print('Bye')
529 523
530 524 # Tests for cell magics support
531 525
532 526 def test_last_blank():
533 527 assert isp.last_blank("") is False
534 528 assert isp.last_blank("abc") is False
535 529 assert isp.last_blank("abc\n") is False
536 530 assert isp.last_blank("abc\na") is False
537 531
538 532 assert isp.last_blank("\n") is True
539 533 assert isp.last_blank("\n ") is True
540 534 assert isp.last_blank("abc\n ") is True
541 535 assert isp.last_blank("abc\n\n") is True
542 536 assert isp.last_blank("abc\nd\n\n") is True
543 537 assert isp.last_blank("abc\nd\ne\n\n") is True
544 538 assert isp.last_blank("abc \n \n \n\n") is True
545 539
546 540
547 541 def test_last_two_blanks():
548 542 assert isp.last_two_blanks("") is False
549 543 assert isp.last_two_blanks("abc") is False
550 544 assert isp.last_two_blanks("abc\n") is False
551 545 assert isp.last_two_blanks("abc\n\na") is False
552 546 assert isp.last_two_blanks("abc\n \n") is False
553 547 assert isp.last_two_blanks("abc\n\n") is False
554 548
555 549 assert isp.last_two_blanks("\n\n") is True
556 550 assert isp.last_two_blanks("\n\n ") is True
557 551 assert isp.last_two_blanks("\n \n") is True
558 552 assert isp.last_two_blanks("abc\n\n ") is True
559 553 assert isp.last_two_blanks("abc\n\n\n") is True
560 554 assert isp.last_two_blanks("abc\n\n \n") is True
561 555 assert isp.last_two_blanks("abc\n\n \n ") is True
562 556 assert isp.last_two_blanks("abc\n\n \n \n") is True
563 557 assert isp.last_two_blanks("abc\nd\n\n\n") is True
564 558 assert isp.last_two_blanks("abc\nd\ne\nf\n\n\n") is True
565 559
566 560
567 561 class CellMagicsCommon(object):
568 562
569 563 def test_whole_cell(self):
570 564 src = "%%cellm line\nbody\n"
571 565 out = self.sp.transform_cell(src)
572 566 ref = "get_ipython().run_cell_magic('cellm', 'line', 'body')\n"
573 567 assert out == ref
574 568
575 569 def test_cellmagic_help(self):
576 570 self.sp.push('%%cellm?')
577 571 assert self.sp.push_accepts_more() is False
578 572
579 573 def tearDown(self):
580 574 self.sp.reset()
581 575
582 576
583 577 class CellModeCellMagics(CellMagicsCommon, unittest.TestCase):
584 578 sp = isp.IPythonInputSplitter(line_input_checker=False)
585 579
586 580 def test_incremental(self):
587 581 sp = self.sp
588 582 sp.push("%%cellm firstline\n")
589 583 assert sp.push_accepts_more() is True # 1
590 584 sp.push("line2\n")
591 585 assert sp.push_accepts_more() is True # 2
592 586 sp.push("\n")
593 587 # This should accept a blank line and carry on until the cell is reset
594 588 assert sp.push_accepts_more() is True # 3
595 589
596 590 def test_no_strip_coding(self):
597 591 src = '\n'.join([
598 592 '%%writefile foo.py',
599 593 '# coding: utf-8',
600 594 'print(u"üñîçø∂é")',
601 595 ])
602 596 out = self.sp.transform_cell(src)
603 597 assert "# coding: utf-8" in out
604 598
605 599
606 600 class LineModeCellMagics(CellMagicsCommon, unittest.TestCase):
607 601 sp = isp.IPythonInputSplitter(line_input_checker=True)
608 602
609 603 def test_incremental(self):
610 604 sp = self.sp
611 605 sp.push("%%cellm line2\n")
612 606 assert sp.push_accepts_more() is True # 1
613 607 sp.push("\n")
614 608 # In this case, a blank line should end the cell magic
615 609 assert sp.push_accepts_more() is False # 2
616 610
617 611
618 612 indentation_samples = [
619 613 ('a = 1', 0),
620 614 ('for a in b:', 4),
621 615 ('def f():', 4),
622 616 ('def f(): #comment', 4),
623 617 ('a = ":#not a comment"', 0),
624 618 ('def f():\n a = 1', 4),
625 619 ('def f():\n return 1', 0),
626 620 ('for a in b:\n'
627 621 ' if a < 0:'
628 622 ' continue', 3),
629 623 ('a = {', 4),
630 624 ('a = {\n'
631 625 ' 1,', 5),
632 626 ('b = """123', 0),
633 627 ('', 0),
634 628 ('def f():\n pass', 0),
635 629 ('class Bar:\n def f():\n pass', 4),
636 630 ('class Bar:\n def f():\n raise', 4),
637 631 ]
638 632
639 633 def test_find_next_indent():
640 634 for code, exp in indentation_samples:
641 635 res = isp.find_next_indent(code)
642 636 msg = "{!r} != {!r} (expected)\n Code: {!r}".format(res, exp, code)
643 637 assert res == exp, msg
General Comments 0
You need to be logged in to leave comments. Login now