1 """Module for interactive demos using IPython. 2 3 This module implements a few classes for running Python scripts interactively 4 in IPython for demonstrations. With very simple markup (a few tags in 5 comments), you can control points where the script stops executing and returns 6 control to IPython. 7 8 9 Provided classes 10 ---------------- 11 12 The classes are (see their docstrings for further details): 13 14 - Demo: pure python demos 15 16 - IPythonDemo: demos with input to be processed by IPython as if it had been 17 typed interactively (so magics work, as well as any other special syntax you 18 may have added via input prefilters). 19 20 - LineDemo: single-line version of the Demo class. These demos are executed 21 one line at a time, and require no markup. 22 23 - IPythonLineDemo: IPython version of the LineDemo class (the demo is 24 executed a line at a time, but processed via IPython). 25 26 - ClearMixin: mixin to make Demo classes with less visual clutter. It 27 declares an empty marquee and a pre_cmd that clears the screen before each 28 block (see Subclassing below). 29 30 - ClearDemo, ClearIPDemo: mixin-enabled versions of the Demo and IPythonDemo 31 classes. 32 33 Inheritance diagram: 34 35 .. inheritance-diagram:: IPython.lib.demo 36 :parts: 3 37 38 Subclassing 39 ----------- 40 41 The classes here all include a few methods meant to make customization by 42 subclassing more convenient. Their docstrings below have some more details: 43 44 - highlight(): format every block and optionally highlight comments and 45 docstring content. 46 47 - marquee(): generates a marquee to provide visible on-screen markers at each 48 block start and end. 49 50 - pre_cmd(): run right before the execution of each block. 51 52 - post_cmd(): run right after the execution of each block. If the block 53 raises an exception, this is NOT called. 54 55 56 Operation 57 --------- 58 59 The file is run in its own empty namespace (though you can pass it a string of 60 arguments as if in a command line environment, and it will see those as 61 sys.argv). But at each stop, the global IPython namespace is updated with the 62 current internal demo namespace, so you can work interactively with the data 63 accumulated so far. 64 65 By default, each block of code is printed (with syntax highlighting) before 66 executing it and you have to confirm execution. This is intended to show the 67 code to an audience first so you can discuss it, and only proceed with 68 execution once you agree. There are a few tags which allow you to modify this 69 behavior. 70 71 The supported tags are: 72 73 # <demo> stop 74 75 Defines block boundaries, the points where IPython stops execution of the 76 file and returns to the interactive prompt. 77 78 You can optionally mark the stop tag with extra dashes before and after the 79 word 'stop', to help visually distinguish the blocks in a text editor: 80 81 # <demo> --- stop --- 82 83 84 # <demo> silent 85 86 Make a block execute silently (and hence automatically). Typically used in 87 cases where you have some boilerplate or initialization code which you need 88 executed but do not want to be seen in the demo. 89 90 # <demo> auto 91 92 Make a block execute automatically, but still being printed. Useful for 93 simple code which does not warrant discussion, since it avoids the extra 94 manual confirmation. 95 96 # <demo> auto_all 97 98 This tag can _only_ be in the first block, and if given it overrides the 99 individual auto tags to make the whole demo fully automatic (no block asks 100 for confirmation). It can also be given at creation time (or the attribute 101 set later) to override what's in the file. 102 103 While _any_ python file can be run as a Demo instance, if there are no stop 104 tags the whole file will run in a single block (no different that calling 105 first %pycat and then %run). The minimal markup to make this useful is to 106 place a set of stop tags; the other tags are only there to let you fine-tune 107 the execution. 108 109 This is probably best explained with the simple example file below. You can 110 copy this into a file named ex_demo.py, and try running it via:: 111 112 from IPython.lib.demo import Demo 113 d = Demo('ex_demo.py') 114 d() 115 116 Each time you call the demo object, it runs the next block. The demo object 117 has a few useful methods for navigation, like again(), edit(), jump(), seek() 118 and back(). It can be reset for a new run via reset() or reloaded from disk 119 (in case you've edited the source) via reload(). See their docstrings below. 120 121 Note: To make this simpler to explore, a file called "demo-exercizer.py" has 122 been added to the "docs/examples/core" directory. Just cd to this directory in 123 an IPython session, and type:: 124 125 %run demo-exercizer.py 126 127 and then follow the directions. 128 129 Example 130 ------- 131 132 The following is a very simple example of a valid demo file. 133 134 :: 135 136 #################### EXAMPLE DEMO <ex_demo.py> ############################### 137 '''A simple interactive demo to illustrate the use of IPython's Demo class.''' 138 139 print('Hello, welcome to an interactive IPython demo.') 140 141 # The mark below defines a block boundary, which is a point where IPython will 142 # stop execution and return to the interactive prompt. The dashes are actually 143 # optional and used only as a visual aid to clearly separate blocks while 144 # editing the demo code. 145 # <demo> stop 146 147 x = 1 148 y = 2 149 150 # <demo> stop 151 152 # the mark below makes this block as silent 153 # <demo> silent 154 155 print('This is a silent block, which gets executed but not printed.') 156 157 # <demo> stop 158 # <demo> auto 159 print('This is an automatic block.') 160 print('It is executed without asking for confirmation, but printed.') 161 z = x + y 162 163 print('z =', x) 164 165 # <demo> stop 166 # This is just another normal block. 167 print('z is now:', z) 168 169 print('bye!') 170 ################### END EXAMPLE DEMO <ex_demo.py> ############################ 171 """ 172 173 174 #***************************************************************************** 175 # Copyright (C) 2005-2006 Fernando Perez. <Fernando.Perez@colorado.edu> 176 # 177 # Distributed under the terms of the BSD License. The full license is in 178 # the file COPYING, distributed as part of this software. 179 # 180 #***************************************************************************** 181 182 import os 183 import re 184 import shlex 185 import sys 186 import pygments 187 from pathlib import Path 188 189 from IPython.utils.text import marquee 190 from IPython.utils import openpy 191 from IPython.utils import py3compat 192 __all__ = ['Demo','IPythonDemo','LineDemo','IPythonLineDemo','DemoError'] 193 194 class DemoError(Exception): pass 195 196 def re_mark(mark): 197 return re.compile(r'^\s*#\s+<demo>\s+%s\s*$' % mark,re.MULTILINE) 198 199 class Demo(object): 200 201 re_stop = re_mark(r'-*\s?stop\s?-*') 202 re_silent = re_mark('silent') 203 re_auto = re_mark('auto') 204 re_auto_all = re_mark('auto_all') 205 206 def __init__(self,src,title='',arg_str='',auto_all=None, format_rst=False, 207 formatter='terminal', style='default'): 208 """Make a new demo object. To run the demo, simply call the object. 209 210 See the module docstring for full details and an example (you can use 211 IPython.Demo? in IPython to see it). 212 213 Inputs: 214 215 - src is either a file, or file-like object, or a 216 string that can be resolved to a filename. 217 218 Optional inputs: 219 220 - title: a string to use as the demo name. Of most use when the demo 221 you are making comes from an object that has no filename, or if you 222 want an alternate denotation distinct from the filename. 223 224 - arg_str(''): a string of arguments, internally converted to a list 225 just like sys.argv, so the demo script can see a similar 226 environment. 227 228 - auto_all(None): global flag to run all blocks automatically without 229 confirmation. This attribute overrides the block-level tags and 230 applies to the whole demo. It is an attribute of the object, and 231 can be changed at runtime simply by reassigning it to a boolean 232 value. 233 234 - format_rst(False): a bool to enable comments and doc strings 235 formatting with pygments rst lexer 236 237 - formatter('terminal'): a string of pygments formatter name to be 238 used. Useful values for terminals: terminal, terminal256, 239 terminal16m 240 241 - style('default'): a string of pygments style name to be used. 242 """ 243 if hasattr(src, "read"): 244 # It seems to be a file or a file-like object 245 self.fname = "from a file-like object" 246 if title == '': 247 self.title = "from a file-like object" 248 else: 249 self.title = title 250 else: 251 # Assume it's a string or something that can be converted to one 252 self.fname = src 253 if title == '': 254 (filepath, filename) = os.path.split(src) 255 self.title = filename 256 else: 257 self.title = title 258 self.sys_argv = [src] + shlex.split(arg_str) 259 self.auto_all = auto_all 260 self.src = src 261 262 try: 263 ip = get_ipython() # this is in builtins whenever IPython is running 264 self.inside_ipython = True 265 except NameError: 266 self.inside_ipython = False 267 268 if self.inside_ipython: 269 # get a few things from ipython. While it's a bit ugly design-wise, 270 # it ensures that things like color scheme and the like are always in 271 # sync with the ipython mode being used. This class is only meant to 272 # be used inside ipython anyways, so it's OK. 273 self.ip_ns = ip.user_ns 274 self.ip_colorize = ip.pycolorize 275 self.ip_showtb = ip.showtraceback 276 self.ip_run_cell = ip.run_cell 277 self.shell = ip 278 279 self.formatter = pygments.formatters.get_formatter_by_name(formatter, 280 style=style) 281 self.python_lexer = pygments.lexers.get_lexer_by_name("py3") 282 self.format_rst = format_rst 283 if format_rst: 284 self.rst_lexer = pygments.lexers.get_lexer_by_name("rst") 285 286 # load user data and initialize data structures 287 self.reload() 288 289 def fload(self): 290 """Load file object.""" 291 # read data and parse into blocks 292 if hasattr(self, 'fobj') and self.fobj is not None: 293 self.fobj.close() 294 if hasattr(self.src, "read"): 295 # It seems to be a file or a file-like object 296 self.fobj = self.src 297 else: 298 # Assume it's a string or something that can be converted to one 299 self.fobj = openpy.open(self.fname) 300 301 def reload(self): 302 """Reload source from disk and initialize state.""" 303 self.fload() 304 305 self.src = "".join(openpy.strip_encoding_cookie(self.fobj)) 306 src_b = [b.strip() for b in self.re_stop.split(self.src) if b] 307 self._silent = [bool(self.re_silent.findall(b)) for b in src_b] 308 self._auto = [bool(self.re_auto.findall(b)) for b in src_b] 309 310 # if auto_all is not given (def. None), we read it from the file 311 if self.auto_all is None: 312 self.auto_all = bool(self.re_auto_all.findall(src_b[0])) 313 else: 314 self.auto_all = bool(self.auto_all) 315 316 # Clean the sources from all markup so it doesn't get displayed when 317 # running the demo 318 src_blocks = [] 319 auto_strip = lambda s: self.re_auto.sub('',s) 320 for i,b in enumerate(src_b): 321 if self._auto[i]: 322 src_blocks.append(auto_strip(b)) 323 else: 324 src_blocks.append(b) 325 # remove the auto_all marker 326 src_blocks[0] = self.re_auto_all.sub('',src_blocks[0]) 327 328 self.nblocks = len(src_blocks) 329 self.src_blocks = src_blocks 330 331 # also build syntax-highlighted source 332 self.src_blocks_colored = list(map(self.highlight,self.src_blocks)) 333 334 # ensure clean namespace and seek offset 335 self.reset() 336 337 def reset(self): 338 """Reset the namespace and seek pointer to restart the demo""" 339 self.user_ns = {} 340 self.finished = False 341 self.block_index = 0 342 343 def _validate_index(self,index): 344 if index<0 or index>=self.nblocks: 345 raise ValueError('invalid block index %s' % index) 346 347 def _get_index(self,index): 348 """Get the current block index, validating and checking status. 349 350 Returns None if the demo is finished""" 351 352 if index is None: 353 if self.finished: 354 print('Demo finished. Use <demo_name>.reset() if you want to rerun it.') 355 return None 356 index = self.block_index 357 else: 358 self._validate_index(index) 359 return index 360 361 def seek(self,index): 362 """Move the current seek pointer to the given block. 363 364 You can use negative indices to seek from the end, with identical 365 semantics to those of Python lists.""" 366 if index<0: 367 index = self.nblocks + index 368 self._validate_index(index) 369 self.block_index = index 370 self.finished = False 371 372 def back(self,num=1): 373 """Move the seek pointer back num blocks (default is 1).""" 374 self.seek(self.block_index-num) 375 376 def jump(self,num=1): 377 """Jump a given number of blocks relative to the current one. 378 379 The offset can be positive or negative, defaults to 1.""" 380 self.seek(self.block_index+num) 381 382 def again(self): 383 """Move the seek pointer back one block and re-execute.""" 384 self.back(1) 385 self() 386 387 def edit(self,index=None): 388 """Edit a block. 389 390 If no number is given, use the last block executed. 391 392 This edits the in-memory copy of the demo, it does NOT modify the 393 original source file. If you want to do that, simply open the file in 394 an editor and use reload() when you make changes to the file. This 395 method is meant to let you change a block during a demonstration for 396 explanatory purposes, without damaging your original script.""" 397 398 index = self._get_index(index) 399 if index is None: 400 return 401 # decrease the index by one (unless we're at the very beginning), so 402 # that the default demo.edit() call opens up the sblock we've last run 403 if index>0: 404 index -= 1 405 406 filename = self.shell.mktempfile(self.src_blocks[index]) 407 self.shell.hooks.editor(filename, 1) 408 with open(Path(filename), "r", encoding="utf-8") as f: 409 new_block = f.read() 410 # update the source and colored block 411 self.src_blocks[index] = new_block 412 self.src_blocks_colored[index] = self.highlight(new_block) 413 self.block_index = index 414 # call to run with the newly edited index 415 self() 416 417 def show(self,index=None): 418 """Show a single block on screen""" 419 420 index = self._get_index(index) 421 if index is None: 422 return 423 424 print(self.marquee('<%s> block # %s (%s remaining)' % 425 (self.title,index,self.nblocks-index-1))) 426 print(self.src_blocks_colored[index]) 427 sys.stdout.flush() 428 429 def show_all(self): 430 """Show entire demo on screen, block by block""" 431 432 fname = self.title 433 title = self.title 434 nblocks = self.nblocks 435 silent = self._silent 436 marquee = self.marquee 437 for index,block in enumerate(self.src_blocks_colored): 438 if silent[index]: 439 print(marquee('<%s> SILENT block # %s (%s remaining)' % 440 (title,index,nblocks-index-1))) 441 else: 442 print(marquee('<%s> block # %s (%s remaining)' % 443 (title,index,nblocks-index-1))) 444 print(block, end=' ') 445 sys.stdout.flush() 446 447 def run_cell(self,source): 448 """Execute a string with one or more lines of code""" 449 450 exec(source, self.user_ns) 451 452 def __call__(self,index=None): 453 """run a block of the demo. 454 455 If index is given, it should be an integer >=1 and <= nblocks. This 456 means that the calling convention is one off from typical Python 457 lists. The reason for the inconsistency is that the demo always 458 prints 'Block n/N, and N is the total, so it would be very odd to use 459 zero-indexing here.""" 460 461 index = self._get_index(index) 462 if index is None: 463 return 464 try: 465 marquee = self.marquee 466 next_block = self.src_blocks[index] 467 self.block_index += 1 468 if self._silent[index]: 469 print(marquee('Executing silent block # %s (%s remaining)' % 470 (index,self.nblocks-index-1))) 471 else: 472 self.pre_cmd() 473 self.show(index) 474 if self.auto_all or self._auto[index]: 475 print(marquee('output:')) 476 else: 477 print(marquee('Press <q> to quit, <Enter> to execute...'), end=' ') 478 ans = py3compat.input().strip() 479 if ans: 480 print(marquee('Block NOT executed')) 481 return 482 try: 483 save_argv = sys.argv 484 sys.argv = self.sys_argv 485 self.run_cell(next_block) 486 self.post_cmd() 487 finally: 488 sys.argv = save_argv 489 490 except: 491 if self.inside_ipython: 492 self.ip_showtb(filename=self.fname) 493 else: 494 if self.inside_ipython: 495 self.ip_ns.update(self.user_ns) 496 497 if self.block_index == self.nblocks: 498 mq1 = self.marquee('END OF DEMO') 499 if mq1: 500 # avoid spurious print if empty marquees are used 501 print() 502 print(mq1) 503 print(self.marquee('Use <demo_name>.reset() if you want to rerun it.')) 504 self.finished = True 505 506 # These methods are meant to be overridden by subclasses who may wish to 507 # customize the behavior of of their demos. 508 def marquee(self,txt='',width=78,mark='*'): 509 """Return the input string centered in a 'marquee'.""" 510 return marquee(txt,width,mark) 511 512 def pre_cmd(self): 513 """Method called before executing each block.""" 514 pass 515 516 def post_cmd(self): 517 """Method called after executing each block.""" 518 pass 519 520 def highlight(self, block): 521 """Method called on each block to highlight it content""" 522 tokens = pygments.lex(block, self.python_lexer) 523 if self.format_rst: 524 from pygments.token import Token 525 toks = [] 526 for token in tokens: 527 if token[0] == Token.String.Doc and len(token[1]) > 6: 528 toks += pygments.lex(token[1][:3], self.python_lexer) 529 # parse doc string content by rst lexer 530 toks += pygments.lex(token[1][3:-3], self.rst_lexer) 531 toks += pygments.lex(token[1][-3:], self.python_lexer) 532 elif token[0] == Token.Comment.Single: 533 toks.append((Token.Comment.Single, token[1][0])) 534 # parse comment content by rst lexer 535 # remove the extra newline added by rst lexer 536 toks += list(pygments.lex(token[1][1:], self.rst_lexer))[:-1] 537 else: 538 toks.append(token) 539 tokens = toks 540 return pygments.format(tokens, self.formatter) 541 542 543 class IPythonDemo(Demo): 544 """Class for interactive demos with IPython's input processing applied. 545 546 This subclasses Demo, but instead of executing each block by the Python 547 interpreter (via exec), it actually calls IPython on it, so that any input 548 filters which may be in place are applied to the input block. 549 550 If you have an interactive environment which exposes special input 551 processing, you can use this class instead to write demo scripts which 552 operate exactly as if you had typed them interactively. The default Demo 553 class requires the input to be valid, pure Python code. 554 """ 555 556 def run_cell(self,source): 557 """Execute a string with one or more lines of code""" 558 559 self.shell.run_cell(source) 560 561 class LineDemo(Demo): 562 """Demo where each line is executed as a separate block. 563 564 The input script should be valid Python code. 565 566 This class doesn't require any markup at all, and it's meant for simple 567 scripts (with no nesting or any kind of indentation) which consist of 568 multiple lines of input to be executed, one at a time, as if they had been 569 typed in the interactive prompt. 570 571 Note: the input can not have *any* indentation, which means that only 572 single-lines of input are accepted, not even function definitions are 573 valid.""" 574 575 def reload(self): 576 """Reload source from disk and initialize state.""" 577 # read data and parse into blocks 578 self.fload() 579 lines = self.fobj.readlines() 580 src_b = [l for l in lines if l.strip()] 581 nblocks = len(src_b) 582 self.src = ''.join(lines) 583 self._silent = [False]*nblocks 584 self._auto = [True]*nblocks 585 self.auto_all = True 586 self.nblocks = nblocks 587 self.src_blocks = src_b 588 589 # also build syntax-highlighted source 590 self.src_blocks_colored = list(map(self.highlight,self.src_blocks)) 591 592 # ensure clean namespace and seek offset 593 self.reset() 594 595 596 class IPythonLineDemo(IPythonDemo,LineDemo): 597 """Variant of the LineDemo class whose input is processed by IPython.""" 598 pass 599 600 601 class ClearMixin(object): 602 """Use this mixin to make Demo classes with less visual clutter. 603 604 Demos using this mixin will clear the screen before every block and use 605 blank marquees. 606 607 Note that in order for the methods defined here to actually override those 608 of the classes it's mixed with, it must go /first/ in the inheritance 609 tree. For example: 610 611 class ClearIPDemo(ClearMixin,IPythonDemo): pass 612 613 will provide an IPythonDemo class with the mixin's features. 614 """ 615 616 def marquee(self,txt='',width=78,mark='*'): 617 """Blank marquee that returns '' no matter what the input.""" 618 return '' 619 620 def pre_cmd(self): 621 """Method called before executing each block. 622 623 This one simply clears the screen.""" 624 from IPython.utils.terminal import _term_clear 625 _term_clear() 626 627 class ClearDemo(ClearMixin,Demo): 628 pass 629 630 631 class ClearIPDemo(ClearMixin,IPythonDemo): 632 pass 633 634 635 def slide(file_path, noclear=False, format_rst=True, formatter="terminal", 636 style="native", auto_all=False, delimiter='...'): 637 if noclear: 638 demo_class = Demo 639 else: 640 demo_class = ClearDemo 641 demo = demo_class(file_path, format_rst=format_rst, formatter=formatter, 642 style=style, auto_all=auto_all) 643 while not demo.finished: 644 demo() 645 try: 646 py3compat.input('\n' + delimiter) 647 except KeyboardInterrupt: 648 exit(1) 649 650 if __name__ == '__main__': 651 import argparse 652 parser = argparse.ArgumentParser(description='Run python demos') 653 parser.add_argument('--noclear', '-C', action='store_true', 654 help='Do not clear terminal on each slide') 655 parser.add_argument('--rst', '-r', action='store_true', 656 help='Highlight comments and dostrings as rst') 657 parser.add_argument('--formatter', '-f', default='terminal', 658 help='pygments formatter name could be: terminal, ' 659 'terminal256, terminal16m') 660 parser.add_argument('--style', '-s', default='default', 661 help='pygments style name') 662 parser.add_argument('--auto', '-a', action='store_true', 663 help='Run all blocks automatically without' 664 'confirmation') 665 parser.add_argument('--delimiter', '-d', default='...', 666 help='slides delimiter added after each slide run') 667 parser.add_argument('file', nargs=1, 668 help='python demo file') 669 args = parser.parse_args() 670 slide(args.file[0], noclear=args.noclear, format_rst=args.rst, 671 formatter=args.formatter, style=args.style, auto_all=args.auto, 672 delimiter=args.delimiter) 673