Format: text/x-python
⬇ Download File (23.9 KB)
   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