1 #!/usr/bin/env python 2 3 """ 4 View annotated sources. 5 6 Copyright (C) 2006 Paul Boddie <paul@boddie.org.uk> 7 8 This software is free software; you can redistribute it and/or 9 modify it under the terms of the GNU General Public License as 10 published by the Free Software Foundation; either version 2 of 11 the License, or (at your option) any later version. 12 13 This software is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public 19 License along with this library; see the file LICENCE.txt 20 If not, write to the Free Software Foundation, Inc., 21 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 22 """ 23 24 from compiler.visitor import ASTVisitor 25 from simplified import * 26 import sys 27 import textwrap 28 29 # Exceptions. 30 31 class ViewerError(SimplifiedError): 32 33 "An error in viewing." 34 35 pass 36 37 # Classes. 38 39 class Viewer(ASTVisitor): 40 41 """ 42 A viewing visitor for AST nodes. 43 """ 44 45 def __init__(self, stream): 46 ASTVisitor.__init__(self) 47 self.cached_files = {} 48 self.printed_lines = {} 49 self.visitor = self 50 self.stream = stream 51 52 def process(self, module): 53 self.dispatch(module) 54 55 def dispatch(self, node): 56 self.dispatch_only(node) 57 ASTVisitor.dispatch(self, node) 58 59 def dispatch_only(self, node, every_time=0): 60 self.print_line(getattr(node, "filename", None), getattr(node, "lineno", None), every_time) 61 62 def print_line(self, filename, lineno, every_time): 63 last_printed = self.printed_lines.get(filename, 0) 64 if lineno > last_printed or every_time: 65 self.stream.write(self.get_line(filename, lineno)) 66 self.printed_lines[filename] = lineno 67 68 def get_line(self, filename, lineno): 69 if filename is None or lineno is None: 70 return "" 71 72 if self.cached_files.has_key(filename): 73 lines = self.cached_files[filename] 74 else: 75 f = open(filename) 76 try: 77 self.cached_files[filename] = lines = f.readlines() 78 finally: 79 f.close() 80 81 try: 82 return lines[lineno - 1] 83 except IndexError: 84 return "" 85 86 def report(self, exc): 87 self.stream.write("Exception was:\n\n" + str(exc.exc) + "\n\n") 88 self.stream.write("Nodes:\n\n") 89 for node in exc.nodes: 90 self.stream.write(repr(node) + "\n") 91 if node is not None: 92 self.dispatch_only(node.original, every_time=1) 93 self.stream.write("\nOriginal node was:\n\n" + repr(exc.nodes[0].original) + "\n") 94 self.stream.write("\nSimplified node was:\n\n") 95 exc.nodes[0].pprint(stream=self.stream) 96 97 # HTML-related output production. 98 99 html_header = """<?xml version="1.0" encoding="iso-8859-15"?> 100 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> 101 <html xmlns="http://www.w3.org/1999/xhtml"> 102 <head> 103 <title>Module</title> 104 <style type="text/css"> 105 body { 106 padding-top: 4em; padding-bottom: 4em; 107 font-size: 14pt; font-family: monospace; 108 background-color: black; color: white; 109 } 110 111 .class { margin-bottom: 1em; } 112 .function { margin-bottom: 1em; } 113 .body { padding-left: 2em; } 114 .keyword { color: yellow; } 115 .comment { color: blue; } 116 .str { color: #FF00FF; } 117 .doc { color: #FF00FF; margin-bottom: 1em; } 118 .invocation a { color: white; text-decoration: none; } 119 120 .popup { 121 display: none; z-index: 2; 122 position: absolute; top: 1em; left: 0.5em; 123 padding: 0.2em; background-color: #000000; 124 } 125 126 .invocations { 127 padding: 0.5em; background-color: #770000; 128 clear: all; 129 } 130 131 .types { 132 padding: 0.5em; background-color: #0000FF; 133 float: right; 134 } 135 136 .raises { 137 padding: 0.5em; background-color: #7700FF; 138 float: right; 139 } 140 141 .scopes { 142 padding: 0.5em; background-color: #007700; 143 float: left; 144 } 145 146 .non-accesses { 147 padding: 0.5em; background-color: #FF0000; 148 float: right; 149 } 150 151 .op, 152 .name, 153 .attr, 154 .conditional 155 { 156 position: relative; 157 } 158 159 .op:hover > .popup, 160 .name:hover > .popup, 161 .attr:hover > .popup, 162 .conditional:hover > .popup 163 { 164 display: block; 165 } 166 167 </style> 168 </head> 169 <body> 170 """ 171 172 html_footer = """</body> 173 </html> 174 """ 175 176 # Browser classes. 177 178 class Browser(ASTVisitor): 179 180 """ 181 A browsing visitor for AST nodes. 182 183 Covered: AssAttr, AssList, AssName, AssTuple, Assign, AugAssign, Break, 184 CallFunc, Class, Compare, Const, Continue, Dict, Discard, For, 185 Function, Getattr, If, Keyword, Lambda, List, Module, Name, Pass, Raise, Return, Slice, 186 Stmt, Subscript, Tuple, While. 187 188 Missing: And, Add, Assert, Backquote, Bitand, Bitor, Bitxor, Decorators, Div, 189 Ellipsis, Exec, FloorDiv, From, Global, Import, Invert, LeftShift, ListComp, ListCompFor, 190 ListCompIf, Mod, Mul, Not, Or, Power, Print, Printnl, RightShift, Sliceobj, 191 Sub, TryExcept, TryFinally, UnaryAdd, UnarySub, Yield. 192 """ 193 194 def __init__(self, stream): 195 ASTVisitor.__init__(self) 196 self.visitor = self 197 self.stream = stream 198 199 def process(self, module): 200 self.stream.write(html_header) 201 self.dispatch(module) 202 self.stream.write(html_footer) 203 204 def dispatch(self, node): 205 try: 206 ASTVisitor.dispatch(self, node) 207 except ViewerError, exc: 208 exc.add(node) 209 raise 210 except Exception, exc: 211 raise ViewerError(exc, node) 212 213 def visitModule(self, node): 214 self.default(node) 215 216 # Statements. 217 218 def visitAssign(self, node): 219 self.stream.write("<div class='assign'>\n") 220 for lvalue in node.nodes: 221 self.dispatch(lvalue) 222 self.stream.write("=\n") 223 self.dispatch(node.expr) 224 self.stream.write("</div>\n") 225 226 def visitAugAssign(self, node): 227 self.stream.write("<div class='augassign'>\n") 228 self.dispatch(node.node) 229 self.stream.write("%s\n" % node.op) 230 self.dispatch(node.expr) 231 self.stream.write("</div>\n") 232 233 def visitBreak(self, node): 234 self.stream.write("<div class='break'>\n") 235 self._keyword("break") 236 self.stream.write("</div>\n") 237 238 def visitClass(self, node): 239 definition = node._node 240 structure = definition.expr.ref 241 self.stream.write("<div class='class' id='%s'>\n" % self._url(structure.full_name())) 242 self.stream.write("<div>\n") 243 self._keyword("class") 244 self._name_start(structure.name) 245 self._popup_start() 246 self._scopes(definition) 247 self._popup_end() 248 self._name_end() 249 bases = structure.bases 250 if bases: 251 self.stream.write("(") 252 first = 1 253 for base in bases: 254 if not first: 255 self.stream.write(",\n") 256 self._name_start(base.name) 257 self._popup_start() 258 self._types(base) 259 self._scopes(base) 260 self._popup_end() 261 self._name_end() 262 first = 0 263 self.stream.write(")") 264 self.stream.write(":\n") 265 self._comment(self._text(structure.full_name())) 266 self.stream.write("</div>\n") 267 268 self.stream.write("<div class='body'>\n") 269 self._doc(node) 270 self.dispatch(node.code) 271 self.stream.write("</div>\n") 272 self.stream.write("</div>\n") 273 274 def visitContinue(self, node): 275 self.stream.write("<div class='continue'>\n") 276 self._keyword("continue") 277 self.stream.write("</div>\n") 278 279 def visitDiscard(self, node): 280 self.stream.write("<div class='discard'>\n") 281 self.default(node) 282 self.stream.write("</div>\n") 283 284 def visitFor(self, node): 285 self.stream.write("<div class='if'>\n") 286 self.stream.write("<div>\n") 287 self._keyword("for") 288 self.dispatch(node.assign) 289 self._keyword("in") 290 self.dispatch(node.list) 291 self.stream.write(":\n") 292 self.stream.write("</div>\n") 293 self.stream.write("<div class='body'>\n") 294 self.dispatch(node.body) 295 self.stream.write("</div>\n") 296 if node.else_ is not None: 297 self.stream.write("<div>\n") 298 self._keyword("else") 299 self.stream.write(":\n") 300 self.stream.write("</div>\n") 301 self.stream.write("<div class='body'>\n") 302 self.dispatch(node.else_) 303 self.stream.write("</div>\n") 304 self.stream.write("</div>\n") 305 306 def visitFunction(self, node): 307 definition = node._node 308 subprogram = definition.expr.ref 309 self.stream.write("<div class='function' id='%s'>\n" % self._url(subprogram.full_name())) 310 self.stream.write("<div>\n") 311 self._keyword("def") 312 self._name_start(subprogram.name) 313 self._popup_start() 314 self._scopes(definition) 315 self._raises(subprogram) 316 self._popup_end() 317 self._name_end() 318 self.stream.write("(") 319 self._parameters(subprogram) 320 self.stream.write(")") 321 self.stream.write(":\n") 322 self._comment(self._text(subprogram.full_name())) 323 self.stream.write("</div>\n") 324 325 self.stream.write("<div class='body'>\n") 326 self._doc(node) 327 self.dispatch(node.code) 328 self.stream.write("</div>\n") 329 self.stream.write("</div>\n") 330 331 def visitIf(self, node): 332 self.stream.write("<div class='if'>\n") 333 first = 1 334 conditional = node._node 335 for compare, stmt in node.tests: 336 self.stream.write("<div>\n") 337 self.stream.write("<span class='conditional'>\n") 338 if first: 339 self._keyword("if") 340 else: 341 self._keyword("elif") 342 self._popup_start() 343 self._invocations(conditional.test) 344 self._popup_end() 345 self.stream.write("</span>\n") 346 self.dispatch(compare) 347 self.stream.write(":\n") 348 self.stream.write("</div>\n") 349 self.stream.write("<div class='body'>\n") 350 self.dispatch(stmt) 351 self.stream.write("</div>\n") 352 if conditional.else_: 353 conditional = conditional.else_[0] 354 else: 355 conditional = None 356 first = 0 357 if node.else_ is not None: 358 self.stream.write("<div>\n") 359 self._keyword("else") 360 self.stream.write(":\n") 361 self.stream.write("</div>\n") 362 self.stream.write("<div class='body'>\n") 363 self.dispatch(node.else_) 364 self.stream.write("</div>\n") 365 self.stream.write("</div>\n") 366 367 def visitPass(self, node): 368 self.stream.write("<div class='pass'>\n") 369 self._keyword("pass") 370 self.stream.write("</div>\n") 371 372 def visitRaise(self, node): 373 self.stream.write("<div class='raise'>\n") 374 self._keyword("raise") 375 self.dispatch(node.expr1) 376 if node.expr2 is not None: 377 self.stream.write(",\n") 378 self.dispatch(node.expr2) 379 if node.expr3 is not None: 380 self.stream.write(",\n") 381 self.dispatch(node.expr3) 382 self.stream.write("</div>\n") 383 384 def visitReturn(self, node): 385 self.stream.write("<div class='return'>\n") 386 self._keyword("return") 387 self.dispatch(node.value) 388 self.stream.write("</div>\n") 389 390 def visitStmt(self, node): 391 self.stream.write("<div class='stmt'>\n") 392 self.default(node) 393 self.stream.write("</div>\n") 394 395 def visitTryExcept(self, node): 396 self.stream.write("<div class='tryexcept'>\n") 397 self.stream.write("<div>\n") 398 self._keyword("try") 399 self.stream.write(":\n") 400 self.stream.write("</div>\n") 401 self.stream.write("<div class='body'>\n") 402 self.dispatch(node.body) 403 self.stream.write("</div>\n") 404 for spec, assign, statement in node.handlers: 405 self.stream.write("<div>\n") 406 self._keyword("except") 407 if spec is not None: 408 self.dispatch(spec) 409 if assign is not None: 410 self.stream.write(",\n") 411 self.dispatch(assign) 412 self.stream.write(":\n") 413 self.stream.write("</div>\n") 414 self.stream.write("<div class='body'>\n") 415 self.dispatch(statement) 416 self.stream.write("</div>\n") 417 if node.else_ is not None: 418 self.stream.write("<div>\n") 419 self._keyword("else") 420 self.stream.write(":\n") 421 self.stream.write("</div>\n") 422 self.stream.write("<div class='body'>\n") 423 self.dispatch(node.else_) 424 self.stream.write("</div>\n") 425 self.stream.write("</div>\n") 426 427 def visitTryFinally(self, node): 428 self.stream.write("<div class='tryfinally'>\n") 429 self.stream.write("<div>\n") 430 self._keyword("try") 431 self.stream.write(":\n") 432 self.stream.write("</div>\n") 433 self.stream.write("<div class='body'>\n") 434 self.dispatch(node.body) 435 self.stream.write("</div>\n") 436 self.stream.write("<div>\n") 437 self._keyword("finally") 438 self.stream.write(":\n") 439 self.stream.write("</div>\n") 440 self.stream.write("<div class='body'>\n") 441 self.dispatch(node.final) 442 self.stream.write("</div>\n") 443 self.stream.write("</div>\n") 444 445 def visitWhile(self, node): 446 self.stream.write("<div class='while'>\n") 447 self.stream.write("<div>\n") 448 self.stream.write("<span class='conditional'>\n") 449 self._keyword("while") 450 self._popup_start() 451 self._invocations(node.test) 452 self._popup_end() 453 self.stream.write("</span>\n") 454 self.dispatch(node.test) 455 self.stream.write(":\n") 456 self.stream.write("</div>\n") 457 self.stream.write("<div class='body'>\n") 458 self.dispatch(node.body) 459 self.stream.write("</div>\n") 460 if node.else_ is not None: 461 self.stream.write("<div>\n") 462 self._keyword("else") 463 self.stream.write(":\n") 464 self.stream.write("</div>\n") 465 self.stream.write("<div class='body'>\n") 466 self.dispatch(node.else_) 467 self.stream.write("</div>\n") 468 self.stream.write("</div>\n") 469 470 # Expressions. 471 472 def visitAssAttr(self, node): 473 self.stream.write("<span class='assattr'>\n") 474 self.dispatch(node.expr) 475 self.stream.write("<span class='attr'>\n") 476 self.stream.write(".%s\n" % self._text(node.attrname)) 477 if hasattr(node, "_node"): 478 self._popup_start() 479 self._types(node._node) 480 self._scopes(node._node) 481 self._popup_end() 482 else: 483 raise ValueError, node 484 self.stream.write("</span>\n") 485 self.stream.write("</span>\n") 486 487 def visitAssList(self, node): 488 self.stream.write("<span class='list'>\n") 489 self.stream.write("[") 490 self._sequence(node) 491 self.stream.write("]\n") 492 self.stream.write("</span>\n") 493 494 def visitAssName(self, node): 495 if hasattr(node, "_node"): 496 self._name_start(node._node.name) 497 self._popup_start() 498 self._types(node._node.expr) 499 self._scopes(node._node) 500 self._popup_end() 501 self._name_end() 502 else: 503 raise ValueError, node 504 self._name(node.name) 505 506 def visitAssTuple(self, node): 507 self.stream.write("<span class='tuple'>\n") 508 self.stream.write("(") 509 self._sequence(node) 510 self.stream.write(")\n") 511 self.stream.write("</span>\n") 512 513 def visitCallFunc(self, node): 514 self.stream.write("<span class='callfunc'>\n") 515 self.dispatch(node.node) 516 self.stream.write("(") 517 first = 1 518 for arg in node.args: 519 if not first: 520 self.stream.write(",\n") 521 self.dispatch(arg) 522 first = 0 523 if node.star_args is not None: 524 if not first: 525 self.stream.write(", *\n") 526 self.dispatch(node.star_args) 527 first = 0 528 if node.dstar_args is not None: 529 if not first: 530 self.stream.write(", **\n") 531 self.dispatch(node.dstar_args) 532 first = 0 533 self.stream.write(")\n") 534 self.stream.write("</span>\n") 535 536 def visitCompare(self, node): 537 self.stream.write("<span class='compare'>\n") 538 self.dispatch(node.expr) 539 for (op_name, expr), _op in map(None, node.ops, node._ops): 540 self.stream.write("<span class='op'>\n") 541 self.stream.write(op_name) 542 self._popup_start() 543 self._op(op_name, _op) 544 self._popup_end() 545 self.stream.write("</span>\n") 546 self.dispatch(expr) 547 self.stream.write("</span>\n") 548 549 def visitConst(self, node): 550 self.stream.write(repr(node.value)) 551 552 def visitGetattr(self, node): 553 self.stream.write("<span class='getattr'>\n") 554 self.dispatch(node.expr) 555 self.stream.write("<span class='attr'>\n") 556 self.stream.write(".%s\n" % self._text(node.attrname)) 557 if hasattr(node, "_node"): 558 self._popup_start() 559 self._types(node._node) 560 self._scopes(node._node) 561 self._popup_end() 562 else: 563 raise ValueError, node 564 self.stream.write("</span>\n") 565 self.stream.write("</span>\n") 566 567 def visitKeyword(self, node): 568 self.stream.write("<span class='keyword'>\n") 569 self.stream.write(node.name) 570 self.stream.write("=") 571 self.dispatch(node.expr) 572 self.stream.write("</span>\n") 573 574 def visitLambda(self, node): 575 definition = node._node 576 subprogram = definition.expr.ref 577 self.stream.write("<span class='lambda'>\n") 578 self._keyword("lambda") 579 self._parameters(subprogram) 580 self.dispatch(node.code) 581 self.stream.write("</span>\n") 582 583 visitList = visitAssList 584 585 def visitName(self, node): 586 if hasattr(node, "_node"): 587 self._name_start(node._node.name) 588 self._popup_start() 589 self._types(node._node) 590 self._scopes(node._node) 591 self._popup_end() 592 self._name_end() 593 else: 594 raise ValueError, node 595 self._name(node.name) 596 597 def visitSlice(self, node): 598 self.stream.write("<span class='slice'>\n") 599 self.dispatch(node.expr) 600 self.stream.write("[") 601 if node.lower: 602 self.dispatch(node.lower) 603 self.stream.write(":") 604 if node.upper: 605 self.dispatch(node.upper) 606 # NOTE: Step? 607 self.stream.write("]") 608 self.stream.write("</span>\n") 609 610 def visitSubscript(self, node): 611 self.stream.write("<span class='subscript'>\n") 612 self.dispatch(node.expr) 613 self.stream.write("[") 614 first = 1 615 for sub in node.subs: 616 if not first: 617 self.stream.write(", ") 618 self.dispatch(sub) 619 first = 0 620 self.stream.write("]") 621 self.stream.write("</span>\n") 622 623 visitTuple = visitAssTuple 624 625 # Output preparation methods. 626 627 def _text(self, text): 628 return text.replace("&", "&").replace("<", "<").replace(">", ">") 629 630 def _attr(self, attr): 631 return self._text(attr).replace("'", "'").replace('"', """) 632 633 def _url(self, url): 634 return self._attr(url).replace("#", "%23").replace("-", "%2d") 635 636 def _comment(self, comment): 637 self.stream.write("<span class='comment'># %s</span>\n" % comment) 638 639 def _keyword(self, kw): 640 self.stream.write("<span class='keyword'>%s</span> " % kw) 641 642 def _doc(self, node): 643 if node.doc is not None: 644 self.stream.write("<pre class='doc'>\n") 645 self.stream.write('"""') 646 output = textwrap.dedent(node.doc.replace('"""', '\\"\\"\\"')) 647 self.stream.write(self._text(output)) 648 self.stream.write('"""') 649 self.stream.write("</pre>\n") 650 651 def _sequence(self, node): 652 first = 1 653 for n in node.nodes: 654 if not first: 655 self.stream.write(",\n") 656 self.dispatch(n) 657 first = 0 658 659 def _parameters(self, subprogram): 660 first = 1 661 for param, default in subprogram.params: 662 if not first: 663 self.stream.write(",\n") 664 self._parameter(subprogram, param, default) 665 first = 0 666 if subprogram.star is not None: 667 if not first: 668 self.stream.write(", *\n") 669 param, default = subprogram.star 670 self._parameter(subprogram, param, default) 671 first = 0 672 if subprogram.dstar is not None: 673 if not first: 674 self.stream.write(", **\n") 675 param, default = subprogram.dstar 676 self._parameter(subprogram, param, default) 677 first = 0 678 679 def _parameter(self, subprogram, param, default): 680 self._name_start(param) 681 if hasattr(subprogram, "paramtypes"): 682 self._popup_start() 683 self._types_list(subprogram.paramtypes[param]) 684 self._popup_end() 685 self._name_end() 686 if default is not None and default.original is not None: 687 self.stream.write("=\n") 688 self.dispatch(default.original) 689 690 def _name(self, name): 691 self.stream.write("<span class='name'>%s</span>\n" % name) 692 693 def _name_start(self, name): 694 self.stream.write("<span class='name'>%s\n" % name) 695 696 def _name_end(self): 697 self.stream.write("</span>\n") 698 699 def _popup_start(self): 700 self.stream.write("<span class='popup'>\n") 701 702 def _popup_end(self): 703 self.stream.write("</span>\n") 704 705 def _op(self, op_name, op): 706 if op is not None: 707 self._invocations(op) 708 709 def _invocations(self, node): 710 if hasattr(node, "invocations"): 711 self._invocations_list(node.invocations) 712 713 def _invocations_list(self, invocations): 714 self.stream.write("<div class='invocations'>\n") 715 for invocation in invocations: 716 fn = invocation.full_name() 717 module = invocation.module.name 718 name = invocation.name 719 structures = [x.name for x in invocation.structures] 720 self.stream.write("<div class='invocation'>") 721 self.stream.write("<a href='%s.html#%s'>" % (self._url(module), self._url(fn))) 722 self.stream.write(self._text(".".join([module] + structures + [name]))) 723 self.stream.write("</a>") 724 self.stream.write("</div>\n") 725 self.stream.write("</div>\n") 726 727 def _types(self, node): 728 if hasattr(node, "types"): 729 if node.types: 730 self._types_list(node.types) 731 else: 732 self.stream.write("<div class='types'>\n") 733 self.stream.write("no types\n") 734 self.stream.write("</div>\n") 735 else: 736 self.stream.write("<div class='types'>\n") 737 self.stream.write("unvisited\n") 738 self.stream.write("</div>\n") 739 740 def _types_list(self, types, style_class="types"): 741 self.stream.write("<div class='%s'>\n" % style_class) 742 for type in types: 743 fn = type.type.full_name() 744 self.stream.write("<div class='type'>") 745 self.stream.write(self._text(fn)) 746 self.stream.write("</div>\n") 747 self.stream.write("</div>\n") 748 749 def _raises(self, node): 750 if hasattr(node, "namespace") and hasattr(node.namespace, "raises") and node.namespace.raises: 751 self._types_list(node.namespace.raises, style_class="raises") 752 753 def _scopes(self, node): 754 if not isinstance(node, LoadName): 755 if hasattr(node, "writes") and node.writes or hasattr(node, "accesses") and node.accesses: 756 self.stream.write("<div class='scopes'>\n") 757 for ref in getattr(node, "writes", getattr(node, "accesses", {})).keys(): 758 fn = ref.full_name() 759 self.stream.write("<div class='scope'>") 760 self.stream.write(self._text(fn)) 761 self.stream.write("</div>\n") 762 self.stream.write("</div>\n") 763 if hasattr(node, "non_accesses") and node.non_accesses: 764 self._types_list(node.non_accesses, style_class="non-accesses") 765 766 # Utility functions. 767 768 def flatten(lists): 769 result = [] 770 for l in lists: 771 for attr in l: 772 if attr not in result: 773 result.append(attr) 774 return result 775 776 # Convenience functions. 777 778 def view(module, stream=None): 779 viewer = Viewer(stream or sys.stdout) 780 viewer.process(module.original) 781 782 def browse(module, stream=None): 783 browser = Browser(stream or sys.stdout) 784 browser.process(module.original) 785 786 def makedoc(module, filename): 787 stream = open(filename, "wb") 788 try: 789 browser = Browser(stream) 790 browser.process(module.original) 791 finally: 792 stream.close() 793 794 # vim: tabstop=4 expandtab shiftwidth=4