python-igraph manual

For using igraph from Python

   Home       Trees       Indices       Help   
Package igraph :: Package app :: Module shell
[hide private]

Source Code for Module igraph.app.shell

  1  """Command-line user interface of igraph 
  2   
  3  The command-line interface launches a Python shell with the igraph 
  4  module automatically imported into the main namespace. This is mostly a 
  5  convenience module and it is used only by the C{igraph} command line 
  6  script which executes a suitable Python shell and automatically imports 
  7  C{igraph}'s classes and functions in the top-level namespace. 
  8   
  9  Supported Python shells are: 
 10   
 11    - IDLE shell (class L{IDLEShell}) 
 12    - IPython shell (class L{IPythonShell}) 
 13    - Classic Python shell (class L{ClassicPythonShell}) 
 14   
 15  The shells are tried in the above mentioned preference order one by 
 16  one, unless the C{global.shells} configuration key is set which 
 17  overrides the default order. IDLE shell is only tried in Windows 
 18  unless explicitly stated by C{global.shells}, since Linux and 
 19  Mac OS X users are likely to invoke igraph from the command line. 
 20  """ 
 21   
 22  from __future__ import print_function 
 23   
 24  import re 
 25  import sys 
 26   
 27  # pylint: disable-msg=W0401 
 28  # W0401: wildcard import. That's exactly what we need for the shell. 
 29  from igraph import __version__, set_progress_handler, set_status_handler 
 30  from igraph.configuration import Configuration 
31 32 33 # pylint: disable-msg=C0103,R0903 34 # C0103: invalid name. Disabled because this is a third-party class. 35 # R0903: too few public methods. 36 -class TerminalController:
37 """ 38 A class that can be used to portably generate formatted output to 39 a terminal. 40 41 `TerminalController` defines a set of instance variables whose 42 values are initialized to the control sequence necessary to 43 perform a given action. These can be simply included in normal 44 output to the terminal: 45 46 >>> term = TerminalController() 47 >>> print 'This is '+term.GREEN+'green'+term.NORMAL 48 This is green 49 50 Alternatively, the `render()` method can used, which replaces 51 '${action}' with the string required to perform 'action': 52 53 >>> term = TerminalController() 54 >>> print term.render('This is ${GREEN}green${NORMAL}') 55 This is green 56 57 If the terminal doesn't support a given action, then the value of 58 the corresponding instance variable will be set to ''. As a 59 result, the above code will still work on terminals that do not 60 support color, except that their output will not be colored. 61 Also, this means that you can test whether the terminal supports a 62 given action by simply testing the truth value of the 63 corresponding instance variable: 64 65 >>> term = TerminalController() 66 >>> if term.CLEAR_SCREEN: 67 ... print 'This terminal supports clearning the screen.' 68 ... 69 70 Finally, if the width and height of the terminal are known, then 71 they will be stored in the `COLS` and `LINES` attributes. 72 73 @author: Edward Loper 74 """ 75 # Cursor movement: 76 BOL = '' #: Move the cursor to the beginning of the line 77 UP = '' #: Move the cursor up one line 78 DOWN = '' #: Move the cursor down one line 79 LEFT = '' #: Move the cursor left one char 80 RIGHT = '' #: Move the cursor right one char 81 82 # Deletion: 83 CLEAR_SCREEN = '' #: Clear the screen and move to home position 84 CLEAR_EOL = '' #: Clear to the end of the line. 85 CLEAR_BOL = '' #: Clear to the beginning of the line. 86 CLEAR_EOS = '' #: Clear to the end of the screen 87 88 # Output modes: 89 BOLD = '' #: Turn on bold mode 90 BLINK = '' #: Turn on blink mode 91 DIM = '' #: Turn on half-bright mode 92 REVERSE = '' #: Turn on reverse-video mode 93 NORMAL = '' #: Turn off all modes 94 95 # Cursor display: 96 HIDE_CURSOR = '' #: Make the cursor invisible 97 SHOW_CURSOR = '' #: Make the cursor visible 98 99 # Terminal size: 100 COLS = None #: Width of the terminal (None for unknown) 101 LINES = None #: Height of the terminal (None for unknown) 102 103 # Foreground colors: 104 BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' 105 106 # Background colors: 107 BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = '' 108 BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = '' 109 110 _STRING_CAPABILITIES = """ 111 BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 112 CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold 113 BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0 114 HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split() 115 _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() 116 _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() 117
118 - def __init__(self, term_stream=sys.stdout):
119 """ 120 Create a `TerminalController` and initialize its attributes 121 with appropriate values for the current terminal. 122 `term_stream` is the stream that will be used for terminal 123 output; if this stream is not a tty, then the terminal is 124 assumed to be a dumb terminal (i.e., have no capabilities). 125 """ 126 # Curses isn't available on all platforms 127 try: 128 import curses 129 except ImportError: 130 return 131 132 # If the stream isn't a tty, then assume it has no capabilities. 133 if not term_stream.isatty(): 134 return 135 136 # Check the terminal type. If we fail, then assume that the 137 # terminal has no capabilities. 138 try: 139 curses.setupterm() 140 except StandardError: 141 return 142 143 # Look up numeric capabilities. 144 self.COLS = curses.tigetnum('cols') 145 self.LINES = curses.tigetnum('lines') 146 147 # Look up string capabilities. 148 for capability in self._STRING_CAPABILITIES: 149 (attrib, cap_name) = capability.split('=') 150 setattr(self, attrib, self._tigetstr(cap_name) or '') 151 152 # Colors 153 set_fg = self._tigetstr('setf') 154 if set_fg: 155 for i, color in zip(range(len(self._COLORS)), self._COLORS): 156 setattr(self, color, self._tparm(set_fg, i) or '') 157 set_fg_ansi = self._tigetstr('setaf') 158 if set_fg_ansi: 159 for i, color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): 160 setattr(self, color, self._tparm(set_fg_ansi, i) or '') 161 set_bg = self._tigetstr('setb') 162 if set_bg: 163 for i, color in zip(range(len(self._COLORS)), self._COLORS): 164 setattr(self, 'BG_'+color, self._tparm(set_bg, i) or '') 165 set_bg_ansi = self._tigetstr('setab') 166 if set_bg_ansi: 167 for i, color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): 168 setattr(self, 'BG_'+color, self._tparm(set_bg_ansi, i) or '') 169 170 @staticmethod
171 - def _tigetstr(cap_name):
172 """Rewrites string capabilities to remove "delays" which are not 173 required for modern terminals""" 174 # String capabilities can include "delays" of the form "$<2>". 175 # For any modern terminal, we should be able to just ignore 176 # these, so strip them out. 177 import curses 178 cap = curses.tigetstr(cap_name) or b'' 179 cap = cap.decode("latin-1") 180 return re.sub(r'\$<\d+>[/*]?', '', cap) 181 182 @staticmethod
183 - def _tparm(cap_name, param):
184 import curses 185 cap = curses.tparm(cap_name.encode("latin-1"), param) or b'' 186 return cap.decode("latin-1") 187
188 - def render(self, template):
189 """ 190 Replace each $-substitutions in the given template string with 191 the corresponding terminal control string (if it's defined) or 192 '' (if it's not). 193 """ 194 return re.sub('r\$\$|\${\w+}', self._render_sub, template) 195
196 - def _render_sub(self, match):
197 """Helper function for L{render}""" 198 s = match.group() 199 if s == '$$': 200 return s 201 else: 202 return getattr(self, s[2:-1])
203
204 205 -class ProgressBar:
206 """ 207 A 2-line progress bar, which looks like:: 208 209 Header 210 20% [===========----------------------------------] 211 212 The progress bar is colored, if the terminal supports color 213 output; and adjusts to the width of the terminal. 214 """ 215 BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}' 216 HEADER = '${BOLD}${CYAN}%s${NORMAL}\n' 217
218 - def __init__(self, term):
219 self.term = term 220 if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL): 221 raise ValueError("Terminal isn't capable enough -- you " 222 "should use a simpler progress display.") 223 self.width = self.term.COLS or 75 224 self.progress_bar = term.render(self.BAR) 225 self.header = self.term.render(self.HEADER % "".center(self.width)) 226 self.cleared = True #: true if we haven't drawn the bar yet. 227 228 self.last_percent = 0 229 self.last_message = "" 230
231 - def update(self, percent=None, message=None):
232 """Updates the progress bar. 233 234 @param percent: the percentage to be shown. If C{None}, the previous 235 value will be used. 236 @param message: the message to be shown above the progress bar. If 237 C{None}, the previous message will be used. 238 """ 239 if self.cleared: 240 sys.stdout.write("\n"+self.header) 241 self.cleared = False 242 243 if message is None: 244 message = self.last_message 245 else: 246 self.last_message = message 247 248 if percent is None: 249 percent = self.last_percent 250 else: 251 self.last_percent = percent 252 253 n = int((self.width-10)*(percent/100.0)) 254 sys.stdout.write( 255 self.term.BOL + self.term.UP + self.term.UP + self.term.CLEAR_EOL + 256 self.term.render(self.HEADER % message.center(self.width)) + 257 (self.progress_bar % (percent, '='*n, '-'*(self.width-10-n))) + "\n" 258 ) 259
260 - def update_message(self, message):
261 """Updates the message of the progress bar. 262 263 @param message: the message to be shown above the progress bar 264 """ 265 return self.update(message=message.strip()) 266
267 - def clear(self):
268 """Clears the progress bar (i.e. removes it from the screen)""" 269 if not self.cleared: 270 sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL + 271 self.term.UP + self.term.CLEAR_EOL + 272 self.term.UP + self.term.CLEAR_EOL) 273 self.cleared = True 274 self.last_percent = 0 275 self.last_message = ""
276
277 278 -class Shell(object):
279 """Superclass of the embeddable shells supported by igraph""" 280
281 - def __init__(self):
282 pass 283
284 - def __call__(self):
285 raise NotImplementedError("abstract class") 286
287 - def supports_progress_bar(self):
288 """Checks whether the shell supports progress bars. 289 290 This is done by checking for the existence of an attribute 291 called C{_progress_handler}.""" 292 return hasattr(self, "_progress_handler") 293
294 - def supports_status_messages(self):
295 """Checks whether the shell supports status messages. 296 297 This is done by checking for the existence of an attribute 298 called C{_status_handler}.""" 299 return hasattr(self, "_status_handler") 300 301 # pylint: disable-msg=E1101
302 - def get_progress_handler(self):
303 """Returns the progress handler (if exists) or None (if not).""" 304 if self.supports_progress_bar(): 305 return self._progress_handler 306 return None 307 308 # pylint: disable-msg=E1101
309 - def get_status_handler(self):
310 """Returns the status handler (if exists) or None (if not).""" 311 if self.supports_status_messages(): 312 return self._status_handler 313 return None
314
315 316 -class IDLEShell(Shell):
317 """IDLE embedded shell interface. 318 319 This class allows igraph to be embedded in IDLE (the Tk Python IDE). 320 321 @todo: no progress bar support yet. Shell/Restart Shell command should 322 re-import igraph again.""" 323
324 - def __init__(self):
325 """Constructor. 326 327 Imports IDLE's embedded shell. The implementation of this method is 328 ripped from idlelib.PyShell.main() after removing the unnecessary 329 parts.""" 330 Shell.__init__(self) 331 332 import idlelib.PyShell 333 334 idlelib.PyShell.use_subprocess = True 335 336 try: 337 sys.ps1 338 except AttributeError: 339 sys.ps1 = '>>> ' 340 341 root = idlelib.PyShell.Tk(className="Idle") 342 idlelib.PyShell.fixwordbreaks(root) 343 root.withdraw() 344 flist = idlelib.PyShell.PyShellFileList(root) 345 if not flist.open_shell(): 346 raise NotImplementedError 347 self._shell = flist.pyshell 348 self._root = root 349
350 - def __call__(self):
351 """Starts the shell""" 352 self._shell.interp.execsource("from igraph import *") 353 self._root.mainloop() 354 self._root.destroy()
355
356 357 -class ConsoleProgressBarMixin(object):
358 """Mixin class for console shells that support a progress bar.""" 359
360 - def __init__(self):
361 try: 362 self.__class__.progress_bar = ProgressBar(TerminalController()) 363 except ValueError: 364 # Terminal is not capable enough, disable progress handler 365 self._disable_handlers() 366 except TypeError: 367 # Probably running in Python 3.x and we hit a str/bytes issue; 368 # as a temporary solution, disable the progress handler 369 self._disable_handlers() 370
371 - def _disable_handlers(self):
372 """Disables the status and progress handlers if the terminal is not 373 capable enough.""" 374 try: 375 del self.__class__._progress_handler 376 except AttributeError: 377 pass 378 try: 379 del self.__class__._status_handler 380 except AttributeError: 381 pass 382 383 @classmethod
384 - def _progress_handler(cls, message, percentage):
385 """Progress bar handler, called when C{igraph} reports the progress 386 of an operation 387 388 @param message: message provided by C{igraph} 389 @param percentage: percentage provided by C{igraph} 390 """ 391 if percentage >= 100: 392 cls.progress_bar.clear() 393 else: 394 cls.progress_bar.update(percentage, message) 395 396 @classmethod
397 - def _status_handler(cls, message):
398 """Status message handler, called when C{igraph} sends a status 399 message to be displayed. 400 401 @param message: message provided by C{igraph} 402 """ 403 cls.progress_bar.update_message(message)
404
405 406 -class IPythonShell(Shell, ConsoleProgressBarMixin):
407 """IPython embedded shell interface. 408 409 This class allows igraph to be embedded in IPython's interactive shell.""" 410
411 - def __init__(self):
412 """Constructor. 413 414 Imports IPython's embedded shell with separator lines removed.""" 415 Shell.__init__(self) 416 ConsoleProgressBarMixin.__init__(self) 417 418 # We cannot use IPShellEmbed here because generator expressions do not 419 # work there (e.g., set(g.degree(x) for x in [1,2,3])) where g comes 420 # from an external context 421 import sys 422 423 from IPython import __version__ as ipython_version 424 self.ipython_version = ipython_version 425 426 try: 427 # IPython >= 0.11 supports this 428 try: 429 from IPython.terminal.ipapp import TerminalIPythonApp 430 except ImportError: 431 from IPython.frontend.terminal.ipapp import TerminalIPythonApp 432 self._shell = TerminalIPythonApp.instance() 433 sys.argv.append("--nosep") 434 except ImportError: 435 # IPython 0.10 and earlier 436 import IPython.Shell 437 self._shell = IPython.Shell.start() 438 self._shell.IP.runsource("from igraph import *") 439 sys.argv.append("-nosep") 440
441 - def __call__(self):
442 """Starts the embedded shell.""" 443 print("igraph %s running inside " % __version__, end="") 444 if self._shell.__class__.__name__ == "TerminalIPythonApp": 445 self._shell.initialize() 446 self._shell.shell.ex("from igraph import *") 447 self._shell.start() 448 else: 449 self._shell.mainloop()
450
451 452 -class ClassicPythonShell(Shell, ConsoleProgressBarMixin):
453 """Classic Python shell interface. 454 455 This class allows igraph to be embedded in Python's shell.""" 456
457 - def __init__(self):
458 """Constructor. 459 460 Imports Python's classic shell""" 461 Shell.__init__(self) 462 ConsoleProgressBarMixin.__init__(self) 463 self._shell = None 464
465 - def __call__(self):
466 """Starts the embedded shell.""" 467 if self._shell is None: 468 from code import InteractiveConsole 469 self._shell = InteractiveConsole() 470 print("igraph %s running inside " % __version__, end="", file=sys.stderr) 471 self._shell.runsource("from igraph import *") 472 473 self._shell.interact()
474
475 476 -def main():
477 """The main entry point for igraph when invoked from the command 478 line shell""" 479 config = Configuration.instance() 480 481 if config.filename: 482 print("Using configuration from %s" % config.filename, file=sys.stderr) 483 else: 484 print("No configuration file, using defaults", file=sys.stderr) 485 486 if config.has_key("shells"): 487 parts = [part.strip() for part in config["shells"].split(",")] 488 shell_classes = [] 489 available_classes = dict([(k, v) for k, v in globals().iteritems() 490 if isinstance(v, type) and issubclass(v, Shell)]) 491 for part in parts: 492 klass = available_classes.get(part, None) 493 if klass is None: 494 print("Warning: unknown shell class `%s'" % part, file=sys.stderr) 495 continue 496 shell_classes.append(klass) 497 else: 498 shell_classes = [IPythonShell, ClassicPythonShell] 499 import platform 500 if platform.system() == "Windows": 501 shell_classes.insert(0, IDLEShell) 502 503 shell = None 504 for shell_class in shell_classes: 505 # pylint: disable-msg=W0703 506 # W0703: catch "Exception" 507 try: 508 shell = shell_class() 509 break 510 except StandardError: 511 # Try the next one 512 if "Classic" in str(shell_class): 513 raise 514 pass 515 516 if isinstance(shell, Shell): 517 if config["verbose"]: 518 if shell.supports_progress_bar(): 519 set_progress_handler(shell.get_progress_handler()) 520 if shell.supports_status_messages(): 521 set_status_handler(shell.get_status_handler()) 522 shell() 523 else: 524 print("No suitable Python shell was found.", file=sys.stderr) 525 print("Check configuration variable `general.shells'.", file=sys.stderr) 526 527 if __name__ == '__main__': 528 sys.exit(main()) 529

   Home       Trees       Indices       Help