Package rekall :: Package ui :: Module text
[frames] | no frames]

Source Code for Module rekall.ui.text

   1  # Rekall Memory Forensics 
   2  # Copyright (C) 2012 Michael Cohen 
   3  # Copyright 2013 Google Inc. All Rights Reserved. 
   4  # 
   5  # This program is free software; you can redistribute it and/or modify 
   6  # it under the terms of the GNU General Public License as published by 
   7  # the Free Software Foundation; either version 2 of the License, or (at 
   8  # your option) any later version. 
   9  # 
  10  # This program is distributed in the hope that it will be useful, but 
  11  # WITHOUT ANY WARRANTY; without even the implied warranty of 
  12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
  13  # General Public License for more details. 
  14  # 
  15  # You should have received a copy of the GNU General Public License 
  16  # along with this program; if not, write to the Free Software 
  17  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
  18  # 
  19   
  20  """This module implements a text based render. 
  21   
  22  A renderer is used by plugins to produce formatted output. 
  23  """ 
  24   
  25  try: 
  26      import curses 
  27      curses.setupterm() 
  28  except Exception:  # curses sometimes raises weird exceptions. 
  29      curses = None 
  30   
  31  import re 
  32  import os 
  33  import subprocess 
  34  import sys 
  35  import tempfile 
  36  import textwrap 
  37   
  38  from rekall import config 
  39  from rekall_lib import registry 
  40  from rekall_lib import utils 
  41   
  42  from rekall.ui import renderer as renderer_module 
  43   
  44   
  45  config.DeclareOption( 
  46      "--pager", default=os.environ.get("PAGER"), group="Interface", 
  47      help="The pager to use when output is larger than a screen full.") 
  48   
  49  config.DeclareOption( 
  50      "--paging_limit", default=None, group="Interface", type="IntParser", 
  51      help="The number of output lines before we invoke the pager.") 
  52   
  53  config.DeclareOption( 
  54      "--colors", default="auto", type="Choices", 
  55      choices=["auto", "yes", "no"], 
  56      group="Interface", help="Color control. If set to auto only output " 
  57      "colors when connected to a terminal.") 
  58   
  59   
  60  HIGHLIGHT_SCHEME = dict( 
  61      important=(u"WHITE", u"RED"), 
  62      good=(u"GREEN", None), 
  63      neutral=(None, None)) 
  64   
  65   
  66  StyleEnum = utils.AttributeDict( 
  67      address="address", 
  68      value="value", 
  69      compact="compact", 
  70      typed="typed",  # Also show type information. 
  71      full="full", 
  72      cow="cow") 
  73   
  74   
  75  # This comes from http://docs.python.org/library/string.html 
  76  # 7.1.3.1. Format Specification Mini-Language 
  77  FORMAT_SPECIFIER_RE = re.compile(r""" 
  78  (?P<fill>[^{}<>=^#bcdeEfFgGnLorsxX0-9])?  # The fill parameter. This can not be 
  79                                            # a format string or it is ambiguous. 
  80  (?P<align>[<>=^])?     # The alignment. 
  81  (?P<sign>[+\- ])?      # Sign extension. 
  82  (?P<hash>\#)?          # Hash means to preceed the whole thing with 0x. 
  83  (?P<zerofill>0)?       # Should numbers be zero filled. 
  84  (?P<width>\d+)?        # The minimum width. 
  85  (?P<comma>,)? 
  86  (?P<precision>.\d+)?   # Precision 
  87  (?P<type>[bcdeEfFgGnorsxXL%])?  # The format string (Not all are supported). 
  88  """, re.X) 
89 90 91 -def ParseFormatSpec(formatstring):
92 if formatstring == "[addrpad]": 93 return dict( 94 style="address", 95 padding="0" 96 ) 97 98 elif formatstring == "[addr]": 99 return {"style": "address"} 100 101 match = FORMAT_SPECIFIER_RE.match(formatstring) 102 result = {} 103 104 width = match.group("width") 105 if width: 106 result["width"] = int(width) 107 108 align = match.group("align") 109 if align == "<": 110 result["align"] = "l" 111 elif align == ">": 112 result["align"] = "r" 113 elif align == "^": 114 result["align"] = "c" 115 116 return result
117
118 119 -class Pager(object):
120 """A wrapper around a pager. 121 122 The pager can be specified by the session. (eg. 123 session.SetParameter("pager", 'less') or in an PAGER environment var. 124 """ 125 # Default encoding is utf8 126 encoding = "utf8" 127
128 - def __init__(self, session=None, term_fd=None):
129 if session == None: 130 raise RuntimeError("Session must be set") 131 132 self.session = session 133 134 # More is the least common denominator of pagers :-(. Less is better, 135 # but most is best! 136 self.pager_command = (session.GetParameter("pager") or 137 os.environ.get("PAGER")) 138 139 if self.pager_command in [None, "-"]: 140 raise AttributeError("Pager command must be specified") 141 142 self.encoding = session.GetParameter("encoding", "UTF-8") 143 self.fd = None 144 self.paging_limit = self.session.GetParameter("paging_limit") 145 self.data = "" 146 147 # Partial results will be directed to this until we hit the 148 # paging_limit, and then we send them to the real pager. This means that 149 # short results do not invoke the pager, but render directly to the 150 # terminal. It probably does not make sense to have term_fd as anything 151 # other than sys.stdout. 152 self.term_fd = term_fd or sys.stdout 153 if not self.term_fd.isatty(): 154 raise AttributeError("Pager can only work on a tty.") 155 156 self.colorizer = Colorizer( 157 self.term_fd, 158 color=self.session.GetParameter("colors"), 159 session=session 160 )
161
162 - def __enter__(self):
163 return self
164
165 - def __exit__(self, *args, **kwargs):
166 # Delete the temp file. 167 try: 168 if self.fd: 169 self.fd.close() 170 os.unlink(self.fd.name) 171 except OSError: 172 pass
173
174 - def GetTempFile(self):
175 if self.fd is not None: 176 return self.fd 177 178 # Make a temporary filename to store output in. 179 self.fd = tempfile.NamedTemporaryFile(prefix="rekall", delete=False) 180 181 return self.fd
182
183 - def write(self, data):
184 # Encode the data according to the output encoding. 185 data = utils.SmartUnicode(data).encode(self.encoding, "replace") 186 if sys.platform == "win32": 187 data = data.replace("\n", "\r\n") 188 189 if self.fd is not None: 190 # Suppress terminal output. Output is buffered in self.fd and will 191 # be sent to the pager. 192 self.fd.write(data) 193 194 # No paging limit specified - just dump to terminal. 195 elif self.paging_limit is None: 196 self.term_fd.write(data) 197 self.term_fd.flush() 198 199 # If there is not enough output yet, just write it to the terminal and 200 # store it locally. 201 elif len(self.data.splitlines()) < self.paging_limit: 202 self.term_fd.write(data) 203 self.term_fd.flush() 204 self.data += data 205 206 # Now create a tempfile and dump the rest of the output there. 207 else: 208 self.term_fd.write( 209 self.colorizer.Render( 210 "Please wait while the rest is paged...", 211 foreground="YELLOW") + "\r\n") 212 self.term_fd.flush() 213 214 fd = self.GetTempFile() 215 fd.write(self.data + data)
216
217 - def isatty(self):
218 return self.term_fd.isatty()
219
220 - def flush(self):
221 """Wait for the pager to be exited.""" 222 if self.fd is None: 223 return 224 225 try: 226 self.fd.flush() 227 except ValueError: 228 pass 229 230 try: 231 args = dict(filename=self.fd.name) 232 # Allow the user to interpolate the filename in a special way, 233 # otherwise just append to the end of the command. 234 if "%" in self.pager_command: 235 pager_command = self.pager_command % args 236 else: 237 pager_command = self.pager_command + " %s" % self.fd.name 238 239 # On windows the file must be closed before the subprocess 240 # can open it. 241 self.fd.close() 242 243 subprocess.call(pager_command, shell=True) 244 245 # Allow the user to break out from waiting for the command. 246 except KeyboardInterrupt: 247 pass 248 249 finally: 250 try: 251 # This will delete the temp file. 252 os.unlink(self.fd.name) 253 except Exception: 254 pass
255
256 257 -class Colorizer(object):
258 """An object which makes its target colorful.""" 259 260 COLORS = u"BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE" 261 COLOR_MAP = dict([(x, i) for i, x in enumerate(COLORS.split())]) 262 263 terminal_capable = False 264
265 - def __init__(self, stream, color="auto", session=None):
266 """Initialize a colorizer. 267 268 Args: 269 stream: The stream to write to. 270 271 color: If "no" we suppress using colors, even if the output stream 272 can support them. 273 """ 274 if session == None: 275 raise RuntimeError("Session must be set") 276 277 self.session = session 278 self.logging = self.session.logging.getChild("colorizer") 279 280 if stream is None: 281 stream = sys.stdout 282 283 # We currently do not support Win32 colors. 284 if curses is None or color == "no": 285 self.terminal_capable = False 286 287 elif color == "yes": 288 self.terminal_capable = True 289 290 elif color == "auto": 291 try: 292 if curses and stream.isatty(): 293 curses.setupterm() 294 self.terminal_capable = True 295 except AttributeError: 296 pass
297
298 - def tparm(self, capabilities, *args):
299 """A simplified version of tigetstr without terminal delays.""" 300 for capability in capabilities: 301 term_string = curses.tigetstr(capability) 302 if term_string is not None: 303 term_string = re.sub(r"\$\<[^>]+>", "", term_string) 304 break 305 306 try: 307 return curses.tparm(term_string, *args) 308 except Exception as e: 309 self.logging.debug("Unable to set tparm: %s" % e) 310 return ""
311
312 - def Render(self, target, foreground=None, background=None):
313 """Decorate the string with the ansii escapes for the color.""" 314 if (not self.terminal_capable or 315 foreground not in self.COLOR_MAP or 316 background not in self.COLOR_MAP): 317 return utils.SmartUnicode(target) 318 319 escape_seq = "" 320 if background: 321 escape_seq += self.tparm( 322 ["setab", "setb"], self.COLOR_MAP[background]) 323 324 if foreground: 325 escape_seq += self.tparm( 326 ["setaf", "setf"], self.COLOR_MAP[foreground]) 327 328 return (escape_seq + utils.SmartUnicode(target) + 329 self.tparm(["sgr0"]))
330
331 332 -class TextObjectRenderer(renderer_module.ObjectRenderer):
333 """Baseclass for all TextRenderer object renderers.""" 334 335 # Fall back renderer for all objects. 336 renders_type = "object" 337 renderers = ["TextRenderer", "WideTextRenderer", "TestRenderer"] 338 339 __metaclass__ = registry.MetaclassRegistry 340 DEFAULT_STYLE = "full" 341 342 @utils.safe_property
343 - def address_size(self):
344 address_size = 14 345 346 # We get the value of the profile via the session state because doing 347 # self.session.profile will trigger profile autodetection even when 348 # it's not needed. 349 if (self.session.HasParameter("profile_obj") and 350 self.session.profile.metadata("arch") == "I386"): 351 address_size = 10 352 353 return address_size
354
355 - def format_address(self, address, **options):
356 result = "%x" % address 357 padding = options.get("padding", " ") 358 if padding == "0": 359 return ("0x" + "0" * max(0, self.address_size - 2 - len(result)) + 360 result) 361 362 return padding * max( 363 0, self.address_size - 2 - len(result)) + "0x" + result
364
365 - def render_header(self, name="", style=StyleEnum.full, hex_width=0, 366 **options):
367 """This should be overloaded to return the header Cell. 368 369 Note that typically the same ObjectRenderer instance will be used to 370 render all Cells in the same column. 371 372 Args: 373 name: The name of the Column. 374 options: The options of the column (i.e. the dict which defines the 375 column). 376 377 Return: 378 A Cell instance containing the formatted Column header. 379 """ 380 header_cell = Cell(unicode(name), width=options.get("width", None)) 381 382 if style == "address" and header_cell.width < self.address_size: 383 header_cell.rewrap(width=self.address_size, align="c") 384 385 self.header_width = max(header_cell.width, len(unicode(name))) 386 header_cell.rewrap(align="c", width=self.header_width) 387 388 # Append a dashed line as a table header separator. 389 header_cell.append_line("-" * self.header_width) 390 391 return header_cell
392
393 - def render_typed(self, target, **options):
394 return Cell(repr(target), **options)
395
396 - def render_full(self, target, **options):
397 return Cell(utils.SmartUnicode(target), **options)
398
399 - def render_address(self, target, width=None, **options):
400 if target is None: 401 return Cell(width=width) 402 403 return Cell( 404 self.format_address(int(target), **options), 405 width=width)
406
407 - def render_compact(self, *args, **kwargs):
408 return self.render_full(*args, **kwargs)
409
410 - def render_value(self, *args, **kwargs):
411 return self.render_full(*args, **kwargs)
412
413 - def render_row(self, target, style=None, **options):
414 """Render the target suitably. 415 416 The default implementation calls a render_STYLE method based on the 417 style keyword arg. 418 419 Args: 420 target: The object to be rendered. 421 422 style: A value from StyleEnum, specifying how the object should 423 be renderered. 424 425 options: A dict containing rendering options. The options are created 426 from the column options, overriden by the row options and finally 427 the cell options. It is ok for an instance to ignore some or all of 428 the options. Some options only make sense in certain Renderer 429 contexts. 430 431 Returns: 432 A Cell instance containing the rendering of target. 433 """ 434 if not style: 435 style = self.DEFAULT_STYLE 436 437 method = getattr(self, "render_%s" % style, None) 438 if not callable(method): 439 raise NotImplementedError( 440 "%s doesn't know how to render style %s." % ( 441 type(self).__name__, style)) 442 443 cell = method(target, **options) 444 if not isinstance(cell, BaseCell): 445 raise RuntimeError("Invalid cell renderer.") 446 447 return cell
448
449 - def render_cow(self, *_, **__):
450 """Renders Bessy the cow.""" 451 cow = ( 452 " |############ \n" 453 " |##### ##### \n" 454 " |## ## \n" 455 " _ |##### ##### \n" 456 " / \\_ |############ \n" 457 " / \\ | \n" 458 " /\\/\\ /\\ _ | /; ;\\ \n" 459 " / \\/ \\/ \\ | __ \\____// \n" 460 " /\\ .- `. \\ \\ | /{_\\_/ `'\\____ \n" 461 " / `-.__ ^ /\\ \\ | \\___ (o) (o) } \n" 462 " / _____________________________/ :--' \n" 463 " ,-,'`@@@@@@@@ @@@@@@ \\_ `__\\ \n" 464 " ;:( @@@@@@@@@ @@@ \\___(o'o) \n" 465 " :: ) @@@@ @@@@@@ ,'@@( `====' \n" 466 " :: : @@@@@: @@@@ `@@@: \n" 467 " :: \\ @@@@@: @@@@@@@) ( '@@@' \n" 468 " :; /\\ / @@@@@@@@@\\ :@@@@@) \n" 469 " ::/ ) {_----------------: :~`,~~; \n" 470 " ;; `; : ) : / `; ; \n" 471 " ;;; : : ; : ; ; : \n" 472 " `'` / : : : : : : \n" 473 " )_ \\__; :_ ; \\_\\ \n" 474 " :__\\ \\ \\ \\ : \\ \n" 475 " `^' `^' `-^-' \n") 476 477 cell = Cell(value=cow, 478 highlights=[(33, 45, u"RED", u"RED"), 479 (88, 93, u"RED", u"RED"), 480 (93, 95, u"WHITE", u"WHITE"), 481 (95, 100, u"RED", u"RED"), 482 (143, 145, u"RED", u"RED"), 483 (145, 153, u"WHITE", u"WHITE"), 484 (153, 155, u"RED", u"RED"), 485 (198, 203, u"RED", u"RED"), 486 (203, 205, u"WHITE", u"WHITE"), 487 (205, 210, u"RED", u"RED"), 488 (253, 265, u"RED", u"RED")]) 489 return cell
490
491 492 -class AttributedStringRenderer(TextObjectRenderer):
493 renders_type = "AttributedString" 494
495 - def render_address(self, *_, **__):
496 raise NotImplementedError("This doesn't make any sense.")
497
498 - def render_full(self, target, **_):
499 return Cell(value=target.value, highlights=target.highlights, 500 colorizer=self.renderer.colorizer)
501
502 - def render_value(self, target, **_):
503 return Cell(value=target.value)
504
505 506 -class CellRenderer(TextObjectRenderer):
507 """This renders a Cell object into a Cell object. 508 509 i.e. it is just a passthrough object renderer for Cell objects. This is 510 useful for rendering nested tables. 511 """ 512 renders_type = "Cell" 513
514 - def render_row(self, target, **_):
515 return target
516
517 518 -class BaseCell(object):
519 """A Cell represents a single entry in a table. 520 521 Cells always have a fixed number of characters in width and may have 522 arbitrary number of characters (lines) for a height. 523 524 The TextTable consists of an array of Cells: 525 526 Cell Cell Cell Cell <----- Headers. 527 Cell Cell Cell Cell <----- Table rows. 528 529 The ObjectRenderer is responsible for turning an arbitrary object into a 530 Cell object. 531 """ 532 533 _width = None 534 _height = None 535 _align = None 536 _lines = None 537 538 # This flag means we have to respect the value of self._width when 539 # rebuilding because it was specified explicitly, either through the 540 # constructor, or through a call to rewrap. 541 width_explicit = False 542 543 # Stretch ("stretch") or push out ("margin", similar to CSS) to desired 544 # width? 545 mode = "stretch" 546 547 __abstract = True 548
549 - def __init__(self, align="l", width=None, **_):
550 self._align = align or "l" 551 self._width = width 552 if self._width: 553 self.width_explicit = True
554
555 - def __iter__(self):
556 return iter(self.lines)
557
558 - def __unicode__(self):
559 return u"\n".join(self.lines)
560 561 @utils.safe_property
562 - def lines(self):
563 if not self._lines: 564 self.rebuild() 565 566 return self._lines
567 568 @utils.safe_property
569 - def width(self):
570 return self._width
571 572 @utils.safe_property
573 - def height(self):
574 return self._height
575 576 @utils.safe_property
577 - def align(self):
578 return self._align
579
580 - def dirty(self):
581 self._lines = None 582 if not self.width_explicit: 583 self._width = None
584
585 - def rebuild(self):
586 raise NotImplementedError("Subclasses must override.")
587
588 - def rewrap(self, width=None, align="l", mode="stretch"):
589 if width is not None: 590 self.width_explicit = True 591 592 if self.width == width and align == self.align and mode == self.mode: 593 return 594 595 self._width = width 596 self._align = align 597 self.mode = mode 598 self.dirty()
599
600 601 -class JoinedCell(BaseCell):
602 """Joins child cells sideways (left to right). 603 604 This is not a replacement for table output! Joined cells are for use when 605 an object renderer needs to display a subtable, or when one needs to pass 606 on wrapping information onto the table, and string concatenation in the 607 Cell class is insufficient. 608 """ 609
610 - def __init__(self, *cells, **kwargs):
611 super(JoinedCell, self).__init__(**kwargs) 612 self.tablesep = kwargs.pop("tablesep", " ") 613 614 if not cells: 615 cells = [Cell("")] 616 617 self.cells = [] 618 for cell in cells: 619 if (isinstance(cell, JoinedCell) 620 and cell.align == self.align 621 and self.mode == self.mode): 622 # As optimization, JoinedCells are not nested if we can just 623 # consume their contents. However, we have to give the child 624 # cell a chance to recalculate and can only do this if the 625 # configurations are compatible. 626 cell.rebuild() 627 self.cells.extend(cell.cells) 628 elif isinstance(cell, BaseCell): 629 self.cells.append(cell) 630 631 elif not isinstance(cell, basestring): 632 raise RuntimeError( 633 "Something went wrong! Cell should be a string.") 634 635 self.rebuild()
636
637 - def rebuild(self):
638 self._height = 0 639 self._lines = [] 640 641 # Figure out how wide the contents are going to be and adjust as 642 # needed. 643 contents_width = 0 644 for cell in self.cells: 645 contents_width += cell.width + len(self.tablesep) 646 647 contents_width = max(0, contents_width - len(self.tablesep)) 648 649 if self.width_explicit or self.width is None: 650 self._width = max(self.width, contents_width) 651 else: 652 self._width = self.width 653 654 adjustment = self._width - contents_width 655 656 # Wrap or pad children. 657 if adjustment and self.mode == "stretch" and self.cells: 658 align = self.align 659 660 if align == "l": 661 child_cell = self.cells[-1] 662 child_cell.rewrap(width=adjustment + child_cell.width) 663 elif align == "r": 664 child_cell = self.cells[0] 665 child_cell.rewrap(width=adjustment + child_cell.width) 666 elif align == "c": 667 self.cells[-1].rewrap( 668 width=(adjustment / 2) + self.cells[-1].width + 669 adjustment % 2) 670 self.cells[0].rewrap( 671 width=(adjustment / 2) + self.cells[0].width) 672 else: 673 raise ValueError( 674 "Invalid alignment %s for JoinedCell." % align) 675 676 # Build up lines from child cell lines. 677 for cell in self.cells: 678 self._height = max(self.height, cell.height) 679 680 for line_no in xrange(self.height): 681 parts = [] 682 for cell in self.cells: 683 try: 684 parts.append(cell.lines[line_no]) 685 except IndexError: 686 parts.append(" " * cell.width) 687 688 line = self.tablesep.join(parts) 689 if self.mode == "margin": 690 if self.align == "l": 691 line += adjustment * " " 692 elif self.align == "r": 693 line = adjustment * " " + line 694 elif self.align == "c": 695 p, r = divmod(adjustment, 2) 696 line = " " * p + line + " " * (p + r) 697 self._lines.append(line)
698
699 - def __repr__(self):
700 return "<JoinedCell align=%s, width=%s, cells=%s>" % ( 701 repr(self.align), repr(self.width), repr(self.cells))
702
703 704 -class StackedCell(BaseCell):
705 """Vertically stack child cells on top of each other. 706 707 This is not a replacement for table output! Stacked cells should be used 708 when one needs to display multiple lines in a single cell, and the text 709 paragraph logic in the Cell class is insufficient. (E.g. rendering faux 710 graphics, such as QR codes and heatmaps.) 711 712 Arguments: 713 table_align: If True (default) will align child cells as columns. 714 NOTE: With this option, child cells must all be JoinedCell 715 instanes and have exactly the same number of children each. 716 """ 717
718 - def __init__(self, *cells, **kwargs):
719 self.table_align = kwargs.pop("table_align", True) 720 super(StackedCell, self).__init__(**kwargs) 721 722 self.cells = [] 723 for cell in cells: 724 if isinstance(cell, StackedCell): 725 self.cells.extend(cell.cells) 726 else: 727 self.cells.append(cell)
728 729 @utils.safe_property
730 - def width(self):
731 if not self._lines: 732 self.rebuild() 733 734 return self._width
735 736 @utils.safe_property
737 - def column_count(self):
738 if not self.table_align: 739 raise AttributeError( 740 "Only works for StackedCells with table_align set to True.") 741 first_row = self.cells[0] 742 if not isinstance(first_row, JoinedCell): 743 raise AttributeError( 744 ("With table_align is set to True, first cell must be a " 745 "JoinedCell")) 746 return len(self.cells[0].cells)
747
748 - def rebuild(self):
749 target_width = 0 750 if self.width_explicit: 751 target_width = self._width 752 753 self._width = 0 754 self._height = 0 755 self._lines = [] 756 757 column_widths = [] 758 if self.table_align: 759 for row in self.cells: 760 if len(row.cells) > len(column_widths): 761 column_widths.extend([0] * ( 762 len(row.cells) - len(column_widths))) 763 764 for column, cell in enumerate(row.cells): 765 w = column_widths[column] 766 if cell.width > w: 767 column_widths[column] = cell.width 768 769 lines = [] 770 for cell in self.cells: 771 for column, width in enumerate(column_widths): 772 try: 773 cell.cells[column].rewrap(width=width) 774 except IndexError: 775 # Turns out, this row doens't have as many cells as we 776 # have columns (common with last rows). 777 break 778 779 if target_width: 780 # Rewrap to fit the target width. 781 cell.rewrap(align="l", width=target_width, mode="margin") 782 else: 783 cell.dirty() # Gotta update them child cells. 784 785 lines.extend(cell.lines) 786 self._height += cell.height 787 self._width = max(self._width, cell.width) 788 789 self._lines = lines
790
791 - def __repr__(self):
792 return "<StackedCell align=%s, _width=%s, cells=%s>" % ( 793 repr(self.align), repr(self._width), repr(self.cells))
794
795 796 -class Cell(BaseCell):
797 """A cell for text, knows how to wrap, preserve paragraphs and colorize.""" 798 _lines = None 799
800 - def __init__(self, value="", highlights=None, colorizer=None, 801 padding=0, **kwargs):
802 super(Cell, self).__init__(**kwargs) 803 self.paragraphs = value.splitlines() 804 self.colorizer = colorizer 805 self.highlights = highlights or [] 806 self.padding = padding or 0 807 808 if not self._width: 809 if self.paragraphs: 810 self._width = max([len(x) for x in self.paragraphs]) 811 else: 812 self._width = 1 813 814 self._width += self.padding
815
816 - def justify_line(self, line):
817 adjust = self.width - len(line) - self.padding 818 819 if self.align == "l": 820 return " " * self.padding + line + " " * adjust, 0 821 elif self.align == "r": 822 return " " * adjust + line + " " * self.padding, adjust 823 elif self.align == "c": 824 radjust, r = divmod(adjust, 2) 825 ladjust = radjust 826 radjust += r 827 828 padding, r = divmod(self.padding, 2) 829 radjust += padding 830 ladjust += padding 831 ladjust += r 832 833 lpad = " " * ladjust 834 rpad = " " * radjust 835 return lpad + line + rpad, 0 836 else: 837 raise ValueError("Invalid cell alignment: %s." % self.align)
838
839 - def highlight_line(self, line, offset, last_highlight):
840 if not self.colorizer.terminal_capable: 841 return line 842 843 if last_highlight: 844 line = last_highlight + line 845 846 limit = offset + len(line) 847 adjust = 0 848 849 for rule in self.highlights: 850 start = rule.get("start") 851 end = rule.get("end") 852 fg = rule.get("fg") 853 bg = rule.get("bg") 854 bold = rule.get("bold") 855 856 if offset <= start <= limit + adjust: 857 escape_seq = "" 858 if fg is not None: 859 if isinstance(fg, basestring): 860 fg = self.colorizer.COLOR_MAP[fg] 861 862 escape_seq += self.colorizer.tparm( 863 ["setaf", "setf"], fg) 864 865 if bg is not None: 866 if isinstance(bg, basestring): 867 bg = self.colorizer.COLOR_MAP[bg] 868 869 escape_seq += self.colorizer.tparm( 870 ["setab", "setb"], bg) 871 872 if bold: 873 escape_seq += self.colorizer.tparm(["bold"]) 874 875 insert_at = start - offset + adjust 876 line = line[:insert_at] + escape_seq + line[insert_at:] 877 878 adjust += len(escape_seq) 879 last_highlight = escape_seq 880 881 if offset <= end <= limit + adjust: 882 escape_seq = self.colorizer.tparm(["sgr0"]) 883 884 insert_at = end - offset + adjust 885 line = line[:insert_at] + escape_seq + line[insert_at:] 886 887 adjust += len(escape_seq) 888 last_highlight = None 889 890 # Always terminate active highlight at the linebreak because we don't 891 # know what's being rendered to our right. We will resume 892 # last_highlight on next line. 893 if last_highlight: 894 line += self.colorizer.tparm(["sgr0"]) 895 return line, last_highlight
896
897 - def rebuild(self):
898 self._lines = [] 899 last_highlight = None 900 901 normalized_highlights = [] 902 for highlight in self.highlights: 903 if isinstance(highlight, dict): 904 normalized_highlights.append(highlight) 905 else: 906 normalized_highlights.append(dict( 907 start=highlight[0], end=highlight[1], 908 fg=highlight[2], bg=highlight[3])) 909 910 self.highlights = sorted(normalized_highlights, 911 key=lambda x: x["start"]) 912 913 offset = 0 914 for paragraph in self.paragraphs: 915 for line in textwrap.wrap(paragraph, self.width): 916 line, adjust = self.justify_line(line) 917 offset += adjust 918 919 if self.colorizer and self.colorizer.terminal_capable: 920 line, last_highlight = self.highlight_line( 921 line=line, offset=offset, last_highlight=last_highlight) 922 923 self._lines.append(line) 924 925 offset += len(paragraph)
926
927 - def dirty(self):
928 self._lines = None
929
930 - def rewrap(self, width=None, align=None, **_):
931 width = width or self.width or max(0, 0, *[len(line) 932 for line in self.lines]) 933 align = align or self.align or "l" 934 935 if (width, align) == (self.width, self.align): 936 return self 937 938 self._width = width 939 self._align = align 940 self.dirty() 941 942 return self
943
944 - def append_line(self, line):
945 self.paragraphs.append(line) 946 self.dirty()
947 948 @utils.safe_property
949 - def height(self):
950 """The number of chars this Cell takes in height.""" 951 return len(self.lines)
952
953 - def __repr__(self):
954 if not self.paragraphs: 955 contents = "None" 956 elif len(self.paragraphs) == 1: 957 contents = repr(self.paragraphs[0]) 958 else: 959 contents = repr("%s..." % self.paragraphs[0]) 960 961 return "<Cell value=%s, align=%s, width=%s>" % ( 962 contents, repr(self.align), repr(self.width))
963
964 965 -class TextColumn(object):
966 """Implementation for text (mostly CLI) tables.""" 967 968 # The object renderer used for this column. 969 object_renderer = None 970
971 - def __init__(self, table=None, renderer=None, session=None, type=None, 972 formatstring=None, **options):
973 if session is None: 974 raise RuntimeError("A session must be provided.") 975 976 self.session = session 977 self.table = table 978 self.renderer = renderer 979 self.header_width = 0 980 981 # Arbitrary column options to be passed to ObjectRenderer() instances. 982 # This allows a plugin to influence the output somewhat in different 983 # output contexts. 984 self.options = ParseFormatSpec(formatstring) if formatstring else {} 985 # Explicit keyword arguments override formatstring. 986 self.options.update(options) 987 988 # For columns which do not explicitly set their type, we can not 989 # determine the type until the first row has been written. NOTE: It is 990 # not supported to change the type of a column after the first row has 991 # been written. 992 if type: 993 self.object_renderer = self.renderer.get_object_renderer( 994 type=type, target_renderer="TextRenderer", **options)
995
996 - def render_header(self):
997 """Renders the cell header. 998 999 Returns a Cell instance for this column header.""" 1000 # If there is a customized object renderer for this column we use that. 1001 if self.object_renderer: 1002 header = self.object_renderer.render_header(**self.options) 1003 else: 1004 # Otherwise we just use the default. 1005 object_renderer = TextObjectRenderer(self.renderer, self.session) 1006 header = object_renderer.render_header(**self.options) 1007 1008 self.header_width = header.width 1009 1010 return header
1011
1012 - def render_row(self, target, **options):
1013 """Renders the current row for the target.""" 1014 # We merge the row options and the column options. This allows a call to 1015 # table_row() to override options. 1016 merged_opts = self.options.copy() 1017 merged_opts.update(options) 1018 1019 if merged_opts.get("nowrap"): 1020 merged_opts.pop("width", None) 1021 1022 if self.object_renderer is not None: 1023 object_renderer = self.object_renderer 1024 else: 1025 object_renderer = self.table.renderer.get_object_renderer( 1026 target=target, type=merged_opts.get("type"), 1027 target_renderer="TextRenderer", **options) 1028 1029 if target is None: 1030 result = Cell(width=merged_opts.get("width")) 1031 else: 1032 result = object_renderer.render_row(target, **merged_opts) 1033 if result is None: 1034 return 1035 1036 result.colorizer = self.renderer.colorizer 1037 1038 # If we should not wrap we are done. 1039 if merged_opts.get("nowrap"): 1040 return result 1041 1042 if "width" in self.options or self.header_width > result.width: 1043 # Rewrap if we have an explicit width (and wrap wasn't turned off). 1044 # Also wrap to pad if the result is actually narrower than the 1045 # header, otherwise it messes up the columns to the right. 1046 result.rewrap(width=self.header_width, 1047 align=merged_opts.get("align", result.align)) 1048 1049 return result
1050 1051 @utils.safe_property
1052 - def name(self):
1053 return self.options.get("name") or self.options.get("cname", "")
1054
1055 1056 -class TextTable(renderer_module.BaseTable):
1057 """A table is a collection of columns. 1058 1059 This table formats all its cells using proportional text font. 1060 """ 1061 1062 column_class = TextColumn 1063 deferred_rows = None 1064
1065 - def __init__(self, auto_widths=False, **options):
1066 super(TextTable, self).__init__(**options) 1067 1068 # Respect the renderer's table separator preference. 1069 self.options.setdefault("tablesep", self.renderer.tablesep) 1070 1071 # Parse the column specs into column class implementations. 1072 self.columns = [] 1073 1074 for column_specs in self.column_specs: 1075 column = self.column_class(session=self.session, table=self, 1076 renderer=self.renderer, **column_specs) 1077 self.columns.append(column) 1078 1079 # Auto-widths mean we calculate the optimal width for each column. 1080 self.auto_widths = auto_widths 1081 1082 # If we want to autoscale each column we must defer rendering. 1083 if auto_widths: 1084 self.deferred_rows = []
1085
1086 - def write_row(self, *cells, **kwargs):
1087 """Writes a row of the table. 1088 1089 Args: 1090 cells: A list of cell contents. Each cell content is a list of lines 1091 in the cell. 1092 """ 1093 highlight = kwargs.pop("highlight", None) 1094 foreground, background = HIGHLIGHT_SCHEME.get( 1095 highlight, (None, None)) 1096 1097 # Iterate over all lines in the row and write it out. 1098 for line in JoinedCell(tablesep=self.options.get("tablesep"), *cells): 1099 self.renderer.write( 1100 self.renderer.colorizer.Render( 1101 line, foreground=foreground, background=background) + "\n")
1102
1103 - def render_header(self, **options):
1104 """Returns a Cell formed by joining all the column headers.""" 1105 # Get each column to write its own header and then we join them all up. 1106 result = [] 1107 for c in self.columns: 1108 merged_opts = c.options.copy() 1109 merged_opts.update(options) 1110 if not merged_opts.get("hidden"): 1111 result.append(c.render_header()) 1112 1113 return JoinedCell( 1114 *result, tablesep=self.options.get("tablesep", " "))
1115
1116 - def get_row(self, *row, **options):
1117 """Format the row into a single Cell spanning all output columns. 1118 1119 Args: 1120 *row: A list of objects to render in the same order as columns are 1121 defined. 1122 1123 Returns: 1124 A single Cell object spanning the entire row. 1125 """ 1126 result = [] 1127 for c, x in zip(self.columns, row): 1128 merged_opts = c.options.copy() 1129 merged_opts.update(options) 1130 if not merged_opts.get("hidden"): 1131 result.append(c.render_row(x, **options) or Cell("")) 1132 1133 return JoinedCell( 1134 *result, tablesep=self.options.get("tablesep", " "))
1135
1136 - def render_row(self, row=None, highlight=None, annotation=False, **options):
1137 """Write the row to the output.""" 1138 if annotation: 1139 self.renderer.format(*row) 1140 elif self.deferred_rows is None: 1141 return self.write_row(self.get_row(*row, **options), 1142 highlight=highlight) 1143 else: 1144 self.deferred_rows.append((row, options))
1145
1146 - def flush(self):
1147 if self.deferred_rows is not None: 1148 # Calculate the optimal widths. 1149 if self.auto_widths: 1150 total_width = self.renderer.GetColumns() - 10 1151 1152 max_widths = [] 1153 for i, column in enumerate(self.columns): 1154 length = 1 1155 for row in self.deferred_rows: 1156 try: 1157 # Render everything on the same line 1158 rendered_lines = column.render_row( 1159 row[0][i], nowrap=1).lines 1160 1161 1162 if rendered_lines: 1163 rendered_line = rendered_lines[0] 1164 else: 1165 rendered_line = "" 1166 1167 length = max(length, len(rendered_line)) 1168 1169 except IndexError: 1170 pass 1171 1172 max_widths.append(length) 1173 1174 # Now we have the maximum widths of each column. The problem is 1175 # about dividing the total_width into the best width so as much 1176 # fits. 1177 sum_of_widths = sum(max_widths) 1178 for column, max_width in zip(self.columns, max_widths): 1179 width = min( 1180 max_width * total_width / sum_of_widths, 1181 max_width + 1) 1182 1183 width = max(width, len(unicode(column.name))) 1184 column.options["width"] = width 1185 1186 # Render the headers now. 1187 if not self.options.get("suppress_headers"): 1188 for line in self.render_header(): 1189 self.renderer.write(line + "\n") 1190 1191 self.session.report_progress("TextRenderer: sorting %(spinner)s") 1192 for row, options in self.deferred_rows: 1193 self.write_row(self.get_row(*row, **options))
1194
1195 1196 -class UnicodeWrapper(object):
1197 """A wrapper around a file like object which guarantees writes in utf8.""" 1198 1199 _isatty = None 1200
1201 - def __init__(self, fd, encoding='utf8'):
1202 self.fd = fd 1203 self.encoding = encoding
1204
1205 - def write(self, data):
1206 data = utils.SmartUnicode(data).encode(self.encoding, "replace") 1207 self.fd.write(data)
1208
1209 - def flush(self):
1210 self.fd.flush()
1211
1212 - def isatty(self):
1213 if self._isatty is None: 1214 try: 1215 self._isatty = self.fd.isatty() 1216 except AttributeError: 1217 self._isatty = False 1218 1219 return self._isatty
1220
1221 1222 -class TextRenderer(renderer_module.BaseRenderer):
1223 """Renderer for the command line that supports paging, colors and progress. 1224 """ 1225 name = "text" 1226 1227 tablesep = " " 1228 paging_limit = None 1229 progress_fd = None 1230 1231 deferred_rows = None 1232 1233 # Render progress with a spinner. 1234 spinner = r"/-\|" 1235 last_spin = 0 1236 last_message_len = 0 1237 1238 table_class = TextTable 1239
1240 - def __init__(self, tablesep=" ", output=None, mode="a+b", fd=None, 1241 **kwargs):
1242 super(TextRenderer, self).__init__(**kwargs) 1243 1244 # Allow the user to dump all output to a file. 1245 self.output = output 1246 if self.output: 1247 # We append the text output for each command. This allows the user 1248 # to just set it once for the session and each new command is 1249 # recorded in the output file. 1250 fd = open(self.output, mode) 1251 1252 if fd == None: 1253 fd = self.session.fd 1254 1255 if fd == None: 1256 try: 1257 fd = Pager(session=self.session) 1258 except AttributeError: 1259 fd = sys.stdout 1260 1261 # Make sure that our output is unicode safe. 1262 self.fd = UnicodeWrapper(fd) 1263 self.tablesep = tablesep 1264 1265 # We keep the data that we produce in memory for while. 1266 self.data = [] 1267 1268 # Write progress to stdout but only if it is a tty. 1269 self.progress_fd = UnicodeWrapper(sys.stdout) 1270 if not self.progress_fd.isatty(): 1271 self.progress_fd = None 1272 1273 self.colorizer = Colorizer( 1274 self.fd, 1275 color=self.session.GetParameter("colors"), 1276 session=self.session) 1277 self.logging = self.session.logging.getChild("renderer.text")
1278
1279 - def section(self, name=None, width=50):
1280 if name is None: 1281 self.write("*" * width + "\n") 1282 else: 1283 pad_len = width - len(name) - 2 # 1 space on each side. 1284 padding = "*" * (pad_len / 2) # Name is centered. 1285 1286 self.write("\n{0} {1} {2}\n".format(padding, name, padding))
1287
1288 - def format(self, formatstring, *data):
1289 """Parse and interpolate the format string. 1290 1291 A format string consists of a string with interpolation markers 1292 embedded. The syntax for an interpolation marker is 1293 {pos:opt1=value,opt2=value}, where pos is the position of the data 1294 element to interpolate, and opt1, opt2 are the options to provide the 1295 object renderer. 1296 1297 For example: 1298 1299 renderer.format("Process {0:style=compact}", task) 1300 1301 For backwards compatibility we support the following syntaxes: 1302 {0:#x} equivalent to {0:style=address} 1303 {1:d} equivalent to {1} 1304 1305 1306 """ 1307 super(TextRenderer, self).format(formatstring, *data) 1308 1309 # Only clear the progress if we share the same output stream as the 1310 # progress. 1311 if self.fd is self.progress_fd: 1312 self.ClearProgress() 1313 1314 default_pos = 0 1315 # Currently use a very simple regex to format - we dont support 1316 # outputting {} chars. 1317 for part in re.split("({.*?})", formatstring): 1318 # Literal. 1319 if not part.startswith("{"): 1320 self.write(part) 1321 continue 1322 1323 # By default use compact style unless specified otherwise. 1324 options = dict(style="compact") 1325 position = None 1326 1327 # Parse the format string - we do not support anything too complex 1328 # now. 1329 m = re.match(r"{(\d+):(.+)}", part) 1330 if m: 1331 position = int(m.group(1)) 1332 option_string = m.group(2) 1333 1334 m = re.match(r"{(\d*)}", part) 1335 if m: 1336 option_string = "" 1337 if not m.group(1): 1338 position = default_pos 1339 default_pos += 1 1340 else: 1341 position = int(m.group(1)) 1342 1343 if position is None: 1344 self.logging.error("Unknown format specifier: %s", part) 1345 continue 1346 1347 # These are backwards compatible hacks. Newer syntax is 1348 # preferred. 1349 if option_string in ["#x", "08x", "8x", "addr"]: 1350 options["style"] = "address" 1351 options["padding"] = "" 1352 1353 elif option_string == "addrpad": 1354 options["style"] = "address" 1355 options["padding"] = "0" 1356 1357 elif "=" in option_string: 1358 for option_part in option_string.split(","): 1359 if "=" in option_part: 1360 key, value = option_part.split("=", 1) 1361 options[key.strip()] = value.strip() 1362 else: 1363 options[option_part] = True 1364 else: 1365 options.update(ParseFormatSpec(option_string)) 1366 1367 # Get the item to be interpolated. 1368 item = data[position] 1369 1370 # Now find the correct object renderer. 1371 obj_renderer = TextObjectRenderer.ForTarget(item, self)( 1372 renderer=self, session=self.session) 1373 1374 self.write(obj_renderer.render_row(item, **options))
1375
1376 - def write(self, data):
1377 self.fd.write(data)
1378
1379 - def flush(self):
1380 super(TextRenderer, self).flush() 1381 self.data = [] 1382 self.ClearProgress() 1383 self.fd.flush()
1384
1385 - def table_header(self, *args, **options):
1386 options["tablesep"] = self.tablesep 1387 super(TextRenderer, self).table_header(*args, **options) 1388 1389 # Skip the headers if there are deferred_rows. 1390 if (self.table.deferred_rows is not None or 1391 self.table.options.get("suppress_headers") or 1392 self.table.auto_widths): 1393 return 1394 1395 for line in self.table.render_header(**options): 1396 self.write(line + "\n")
1397
1398 - def table_row(self, *args, **kwargs):
1399 """Outputs a single row of a table. 1400 1401 Text tables support these additional kwargs: 1402 highlight: Highlights this raw according to the color scheme (e.g. 1403 important, good...) 1404 """ 1405 super(TextRenderer, self).table_row(*args, **kwargs) 1406 self.RenderProgress(message=None)
1407
1408 - def GetColumns(self):
1409 if curses: 1410 return curses.tigetnum('cols') 1411 1412 return int(os.environ.get("COLUMNS", 80))
1413
1414 - def RenderProgress(self, message=" %(spinner)s", *args, **kwargs):
1415 if super(TextRenderer, self).RenderProgress(**kwargs): 1416 self.last_spin += 1 1417 if not message: 1418 return 1419 1420 # Only expand variables when we need to. 1421 if "%(" in message: 1422 kwargs["spinner"] = self.spinner[ 1423 self.last_spin % len(self.spinner)] 1424 1425 message = message % kwargs 1426 elif args: 1427 format_args = [] 1428 for arg in args: 1429 if callable(arg): 1430 format_args.append(arg()) 1431 else: 1432 format_args.append(arg) 1433 1434 message = message % tuple(format_args) 1435 1436 self.ClearProgress() 1437 1438 message = " " + message + "\r" 1439 1440 # Truncate the message to the terminal width to avoid wrapping. 1441 message = message[:self.GetColumns()] 1442 1443 self.last_message_len = len(message) 1444 1445 self._RenderProgress(message) 1446 1447 return True
1448
1449 - def _RenderProgress(self, message):
1450 """Actually write the progress message. 1451 1452 This can be overwritten by renderers to deliver the progress messages 1453 elsewhere. 1454 """ 1455 if self.progress_fd is not None: 1456 self.progress_fd.write(message) 1457 self.progress_fd.flush()
1458
1459 - def ClearProgress(self):
1460 """Delete the last progress message.""" 1461 if self.progress_fd is None: 1462 return 1463 1464 # Wipe the last message. 1465 self.progress_fd.write("\r" + " " * self.last_message_len + "\r") 1466 self.progress_fd.flush()
1467
1468 - def open(self, directory=None, filename=None, mode="rb"):
1469 if filename is None and directory is None: 1470 raise IOError("Must provide a filename") 1471 1472 if directory is None: 1473 directory, filename = os.path.split(filename) 1474 1475 filename = utils.SmartStr(filename) or "Unknown%s" % self._object_id 1476 1477 # Filter the filename for illegal chars. 1478 filename = re.sub( 1479 r"[^a-zA-Z0-9_.@{}\[\]\- ]", 1480 lambda x: "%" + x.group(0).encode("hex"), 1481 filename) 1482 1483 if directory: 1484 filename = os.path.join(directory, "./", filename) 1485 1486 if "w" in mode: 1487 try: 1488 os.makedirs(directory) 1489 except (OSError, IOError): 1490 pass 1491 1492 return open(filename, mode)
1493
1494 1495 -class TestRenderer(TextRenderer):
1496 """A special renderer which makes parsing the output of tables easier.""" 1497 name = "test" 1498
1499 - def __init__(self, **kwargs):
1500 super(TestRenderer, self).__init__(tablesep="||", **kwargs)
1501
1502 - def GetColumns(self):
1503 # Return a predictable and stable width. 1504 return 138
1505
1506 1507 -class WideTextRenderer(TextRenderer):
1508 """A Renderer which explodes tables into wide formatted records.""" 1509 1510 name = "wide" 1511
1512 - def __init__(self, **kwargs):
1513 super(WideTextRenderer, self).__init__(**kwargs) 1514 1515 self.delegate_renderer = TextRenderer(**kwargs)
1516
1517 - def __enter__(self):
1518 self.delegate_renderer.__enter__() 1519 self.delegate_renderer.table_header([ 1520 dict(name="Key", width=15), 1521 dict(name="Value") 1522 ], suppress_headers=True) 1523 1524 return super(WideTextRenderer, self).__enter__()
1525
1526 - def __exit__(self, exc_type, exc_value, trace):
1527 self.delegate_renderer.__exit__(exc_type, exc_value, trace) 1528 return super(WideTextRenderer, self).__exit__( 1529 exc_type, exc_value, trace)
1530
1531 - def table_header(self, *args, **options):
1532 options["suppress_headers"] = True 1533 super(WideTextRenderer, self).table_header(*args, **options)
1534
1535 - def table_row(self, *row, **options):
1536 self.section() 1537 values = [c.render_row(x) for c, x in zip(self.table.columns, row)] 1538 1539 for c, cell in zip(self.table.columns, values): 1540 column_name = (getattr(c.object_renderer, "name", None) or 1541 c.options.get("name")) 1542 1543 # Skip empty columns. 1544 if not cell.lines: 1545 continue 1546 1547 self.delegate_renderer.table_row(column_name, cell, **options)
1548
1549 1550 -class TreeNodeObjectRenderer(TextObjectRenderer):
1551 renders_type = "TreeNode" 1552
1553 - def __init__(self, renderer=None, session=None, **options):
1554 self.max_depth = options.pop("max_depth", 10) 1555 child_spec = options.pop("child", None) 1556 if child_spec: 1557 child_type = child_spec.get("type", "object") 1558 self.child = self.ByName(child_type, renderer)( 1559 renderer, session=session, **child_spec) 1560 1561 if not self.child: 1562 raise AttributeError("Child %s of TreeNode was not found." % 1563 child_type) 1564 else: 1565 self.child = None 1566 1567 super(TreeNodeObjectRenderer, self).__init__( 1568 renderer, session=session, **options)
1569
1570 - def render_header(self, **options):
1571 if self.child: 1572 heading = JoinedCell(self.child.render_header(**options)) 1573 else: 1574 heading = super(TreeNodeObjectRenderer, self).render_header( 1575 **options) 1576 1577 self.heading_width = heading.width 1578 return heading
1579
1580 - def render_row(self, target, depth=0, child=None, **options):
1581 if not child: 1582 child = {} 1583 1584 if self.child: 1585 child_renderer = self.child 1586 else: 1587 child_renderer = self.ForTarget(target, renderer=self.renderer)( 1588 session=self.session, renderer=self.renderer) 1589 1590 child_cell = child_renderer.render_row(target, **child) 1591 child_cell.colorizer = self.renderer.colorizer 1592 1593 padding = Cell("." * depth) 1594 result = JoinedCell(padding, child_cell) 1595 1596 return result
1597
1598 1599 -class DividerObjectRenderer(TextObjectRenderer):
1600 renders_type = "Divider" 1601
1602 - def __init__(self, renderer=None, session=None, **options):
1603 child_spec = options.pop("child", None) 1604 if child_spec: 1605 child_type = child_spec.get("type", "object") 1606 self.child = self.ByName(child_type, renderer)( 1607 renderer, session=session, **child_spec) 1608 1609 if not self.child: 1610 raise AttributeError("Child %s of Divider was not found." % 1611 child_type) 1612 else: 1613 self.child = None 1614 1615 super(DividerObjectRenderer, self).__init__( 1616 renderer, session=session, **options)
1617
1618 - def render_header(self, **options):
1619 self.header_width = 0 1620 return Cell("")
1621
1622 - def render_row(self, target, child=None, **options):
1623 last_row = self.renderer.table.options.get("last_row") 1624 if last_row == target: 1625 return Cell("") 1626 1627 self.renderer.table.options["last_row"] = target 1628 1629 if not child: 1630 child = dict(wrap=False) 1631 1632 if self.child: 1633 child_renderer = self.child 1634 else: 1635 child_renderer = self.ForTarget(target, renderer=self.renderer)( 1636 session=self.session, renderer=self.renderer) 1637 1638 child_cell = child_renderer.render_row(target, **child) 1639 child_cell.colorizer = self.renderer.colorizer 1640 1641 return StackedCell( 1642 Cell("-" * child_cell.width), 1643 child_cell, 1644 Cell("-" * child_cell.width), 1645 table_align=False)
1646