1"""Class for printing reports on profiled python code.""" 2 3# Written by James Roskind 4# Based on prior profile module by Sjoerd Mullender... 5# which was hacked somewhat by: Guido van Rossum 6 7# Copyright Disney Enterprises, Inc. All Rights Reserved. 8# Licensed to PSF under a Contributor Agreement 9# 10# Licensed under the Apache License, Version 2.0 (the "License"); 11# you may not use this file except in compliance with the License. 12# You may obtain a copy of the License at 13# 14# http://www.apache.org/licenses/LICENSE-2.0 15# 16# Unless required by applicable law or agreed to in writing, software 17# distributed under the License is distributed on an "AS IS" BASIS, 18# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 19# either express or implied. See the License for the specific language 20# governing permissions and limitations under the License. 21 22 23import sys 24import os 25import time 26import marshal 27import re 28 29from enum import Enum 30from functools import cmp_to_key 31from dataclasses import dataclass 32from typing import Dict 33 34__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"] 35 36class SortKey(str, Enum): 37 CALLS = 'calls', 'ncalls' 38 CUMULATIVE = 'cumulative', 'cumtime' 39 FILENAME = 'filename', 'module' 40 LINE = 'line' 41 NAME = 'name' 42 NFL = 'nfl' 43 PCALLS = 'pcalls' 44 STDNAME = 'stdname' 45 TIME = 'time', 'tottime' 46 47 def __new__(cls, *values): 48 value = values[0] 49 obj = str.__new__(cls, value) 50 obj._value_ = value 51 for other_value in values[1:]: 52 cls._value2member_map_[other_value] = obj 53 obj._all_values = values 54 return obj 55 56 57@dataclass(unsafe_hash=True) 58class FunctionProfile: 59 ncalls: int 60 tottime: float 61 percall_tottime: float 62 cumtime: float 63 percall_cumtime: float 64 file_name: str 65 line_number: int 66 67@dataclass(unsafe_hash=True) 68class StatsProfile: 69 '''Class for keeping track of an item in inventory.''' 70 total_tt: float 71 func_profiles: Dict[str, FunctionProfile] 72 73class Stats: 74 """This class is used for creating reports from data generated by the 75 Profile class. It is a "friend" of that class, and imports data either 76 by direct access to members of Profile class, or by reading in a dictionary 77 that was emitted (via marshal) from the Profile class. 78 79 The big change from the previous Profiler (in terms of raw functionality) 80 is that an "add()" method has been provided to combine Stats from 81 several distinct profile runs. Both the constructor and the add() 82 method now take arbitrarily many file names as arguments. 83 84 All the print methods now take an argument that indicates how many lines 85 to print. If the arg is a floating point number between 0 and 1.0, then 86 it is taken as a decimal percentage of the available lines to be printed 87 (e.g., .1 means print 10% of all available lines). If it is an integer, 88 it is taken to mean the number of lines of data that you wish to have 89 printed. 90 91 The sort_stats() method now processes some additional options (i.e., in 92 addition to the old -1, 0, 1, or 2 that are respectively interpreted as 93 'stdname', 'calls', 'time', and 'cumulative'). It takes either an 94 arbitrary number of quoted strings or SortKey enum to select the sort 95 order. 96 97 For example sort_stats('time', 'name') or sort_stats(SortKey.TIME, 98 SortKey.NAME) sorts on the major key of 'internal function time', and on 99 the minor key of 'the name of the function'. Look at the two tables in 100 sort_stats() and get_sort_arg_defs(self) for more examples. 101 102 All methods return self, so you can string together commands like: 103 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\ 104 print_stats(5).print_callers(5) 105 """ 106 107 def __init__(self, *args, stream=None): 108 self.stream = stream or sys.stdout 109 if not len(args): 110 arg = None 111 else: 112 arg = args[0] 113 args = args[1:] 114 self.init(arg) 115 self.add(*args) 116 117 def init(self, arg): 118 self.all_callees = None # calc only if needed 119 self.files = [] 120 self.fcn_list = None 121 self.total_tt = 0 122 self.total_calls = 0 123 self.prim_calls = 0 124 self.max_name_len = 0 125 self.top_level = set() 126 self.stats = {} 127 self.sort_arg_dict = {} 128 self.load_stats(arg) 129 try: 130 self.get_top_level_stats() 131 except Exception: 132 print("Invalid timing data %s" % 133 (self.files[-1] if self.files else ''), file=self.stream) 134 raise 135 136 def load_stats(self, arg): 137 if arg is None: 138 self.stats = {} 139 return 140 elif isinstance(arg, str): 141 with open(arg, 'rb') as f: 142 self.stats = marshal.load(f) 143 try: 144 file_stats = os.stat(arg) 145 arg = time.ctime(file_stats.st_mtime) + " " + arg 146 except: # in case this is not unix 147 pass 148 self.files = [arg] 149 elif hasattr(arg, 'create_stats'): 150 arg.create_stats() 151 self.stats = arg.stats 152 arg.stats = {} 153 if not self.stats: 154 raise TypeError("Cannot create or construct a %r object from %r" 155 % (self.__class__, arg)) 156 return 157 158 def get_top_level_stats(self): 159 for func, (cc, nc, tt, ct, callers) in self.stats.items(): 160 self.total_calls += nc 161 self.prim_calls += cc 162 self.total_tt += tt 163 if ("jprofile", 0, "profiler") in callers: 164 self.top_level.add(func) 165 if len(func_std_string(func)) > self.max_name_len: 166 self.max_name_len = len(func_std_string(func)) 167 168 def add(self, *arg_list): 169 if not arg_list: 170 return self 171 for item in reversed(arg_list): 172 if type(self) != type(item): 173 item = Stats(item) 174 self.files += item.files 175 self.total_calls += item.total_calls 176 self.prim_calls += item.prim_calls 177 self.total_tt += item.total_tt 178 for func in item.top_level: 179 self.top_level.add(func) 180 181 if self.max_name_len < item.max_name_len: 182 self.max_name_len = item.max_name_len 183 184 self.fcn_list = None 185 186 for func, stat in item.stats.items(): 187 if func in self.stats: 188 old_func_stat = self.stats[func] 189 else: 190 old_func_stat = (0, 0, 0, 0, {},) 191 self.stats[func] = add_func_stats(old_func_stat, stat) 192 return self 193 194 def dump_stats(self, filename): 195 """Write the profile data to a file we know how to load back.""" 196 with open(filename, 'wb') as f: 197 marshal.dump(self.stats, f) 198 199 # list the tuple indices and directions for sorting, 200 # along with some printable description 201 sort_arg_dict_default = { 202 "calls" : (((1,-1), ), "call count"), 203 "ncalls" : (((1,-1), ), "call count"), 204 "cumtime" : (((3,-1), ), "cumulative time"), 205 "cumulative": (((3,-1), ), "cumulative time"), 206 "filename" : (((4, 1), ), "file name"), 207 "line" : (((5, 1), ), "line number"), 208 "module" : (((4, 1), ), "file name"), 209 "name" : (((6, 1), ), "function name"), 210 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"), 211 "pcalls" : (((0,-1), ), "primitive call count"), 212 "stdname" : (((7, 1), ), "standard name"), 213 "time" : (((2,-1), ), "internal time"), 214 "tottime" : (((2,-1), ), "internal time"), 215 } 216 217 def get_sort_arg_defs(self): 218 """Expand all abbreviations that are unique.""" 219 if not self.sort_arg_dict: 220 self.sort_arg_dict = dict = {} 221 bad_list = {} 222 for word, tup in self.sort_arg_dict_default.items(): 223 fragment = word 224 while fragment: 225 if not fragment: 226 break 227 if fragment in dict: 228 bad_list[fragment] = 0 229 break 230 dict[fragment] = tup 231 fragment = fragment[:-1] 232 for word in bad_list: 233 del dict[word] 234 return self.sort_arg_dict 235 236 def sort_stats(self, *field): 237 if not field: 238 self.fcn_list = 0 239 return self 240 if len(field) == 1 and isinstance(field[0], int): 241 # Be compatible with old profiler 242 field = [ {-1: "stdname", 243 0: "calls", 244 1: "time", 245 2: "cumulative"}[field[0]] ] 246 elif len(field) >= 2: 247 for arg in field[1:]: 248 if type(arg) != type(field[0]): 249 raise TypeError("Can't have mixed argument type") 250 251 sort_arg_defs = self.get_sort_arg_defs() 252 253 sort_tuple = () 254 self.sort_type = "" 255 connector = "" 256 for word in field: 257 if isinstance(word, SortKey): 258 word = word.value 259 sort_tuple = sort_tuple + sort_arg_defs[word][0] 260 self.sort_type += connector + sort_arg_defs[word][1] 261 connector = ", " 262 263 stats_list = [] 264 for func, (cc, nc, tt, ct, callers) in self.stats.items(): 265 stats_list.append((cc, nc, tt, ct) + func + 266 (func_std_string(func), func)) 267 268 stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare)) 269 270 self.fcn_list = fcn_list = [] 271 for tuple in stats_list: 272 fcn_list.append(tuple[-1]) 273 return self 274 275 def reverse_order(self): 276 if self.fcn_list: 277 self.fcn_list.reverse() 278 return self 279 280 def strip_dirs(self): 281 oldstats = self.stats 282 self.stats = newstats = {} 283 max_name_len = 0 284 for func, (cc, nc, tt, ct, callers) in oldstats.items(): 285 newfunc = func_strip_path(func) 286 if len(func_std_string(newfunc)) > max_name_len: 287 max_name_len = len(func_std_string(newfunc)) 288 newcallers = {} 289 for func2, caller in callers.items(): 290 newcallers[func_strip_path(func2)] = caller 291 292 if newfunc in newstats: 293 newstats[newfunc] = add_func_stats( 294 newstats[newfunc], 295 (cc, nc, tt, ct, newcallers)) 296 else: 297 newstats[newfunc] = (cc, nc, tt, ct, newcallers) 298 old_top = self.top_level 299 self.top_level = new_top = set() 300 for func in old_top: 301 new_top.add(func_strip_path(func)) 302 303 self.max_name_len = max_name_len 304 305 self.fcn_list = None 306 self.all_callees = None 307 return self 308 309 def calc_callees(self): 310 if self.all_callees: 311 return 312 self.all_callees = all_callees = {} 313 for func, (cc, nc, tt, ct, callers) in self.stats.items(): 314 if not func in all_callees: 315 all_callees[func] = {} 316 for func2, caller in callers.items(): 317 if not func2 in all_callees: 318 all_callees[func2] = {} 319 all_callees[func2][func] = caller 320 return 321 322 #****************************************************************** 323 # The following functions support actual printing of reports 324 #****************************************************************** 325 326 # Optional "amount" is either a line count, or a percentage of lines. 327 328 def eval_print_amount(self, sel, list, msg): 329 new_list = list 330 if isinstance(sel, str): 331 try: 332 rex = re.compile(sel) 333 except re.error: 334 msg += " <Invalid regular expression %r>\n" % sel 335 return new_list, msg 336 new_list = [] 337 for func in list: 338 if rex.search(func_std_string(func)): 339 new_list.append(func) 340 else: 341 count = len(list) 342 if isinstance(sel, float) and 0.0 <= sel < 1.0: 343 count = int(count * sel + .5) 344 new_list = list[:count] 345 elif isinstance(sel, int) and 0 <= sel < count: 346 count = sel 347 new_list = list[:count] 348 if len(list) != len(new_list): 349 msg += " List reduced from %r to %r due to restriction <%r>\n" % ( 350 len(list), len(new_list), sel) 351 352 return new_list, msg 353 354 def get_stats_profile(self): 355 """This method returns an instance of StatsProfile, which contains a mapping 356 of function names to instances of FunctionProfile. Each FunctionProfile 357 instance holds information related to the function's profile such as how 358 long the function took to run, how many times it was called, etc... 359 """ 360 func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys()) 361 if not func_list: 362 return StatsProfile(0, {}) 363 364 total_tt = float(f8(self.total_tt)) 365 func_profiles = {} 366 stats_profile = StatsProfile(total_tt, func_profiles) 367 368 for func in func_list: 369 cc, nc, tt, ct, callers = self.stats[func] 370 file_name, line_number, func_name = func 371 ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc)) 372 tottime = float(f8(tt)) 373 percall_tottime = -1 if nc == 0 else float(f8(tt/nc)) 374 cumtime = float(f8(ct)) 375 percall_cumtime = -1 if cc == 0 else float(f8(ct/cc)) 376 func_profile = FunctionProfile( 377 ncalls, 378 tottime, # time spent in this function alone 379 percall_tottime, 380 cumtime, # time spent in the function plus all functions that this function called, 381 percall_cumtime, 382 file_name, 383 line_number 384 ) 385 func_profiles[func_name] = func_profile 386 387 return stats_profile 388 389 def get_print_list(self, sel_list): 390 width = self.max_name_len 391 if self.fcn_list: 392 stat_list = self.fcn_list[:] 393 msg = " Ordered by: " + self.sort_type + '\n' 394 else: 395 stat_list = list(self.stats.keys()) 396 msg = " Random listing order was used\n" 397 398 for selection in sel_list: 399 stat_list, msg = self.eval_print_amount(selection, stat_list, msg) 400 401 count = len(stat_list) 402 403 if not stat_list: 404 return 0, stat_list 405 print(msg, file=self.stream) 406 if count < len(self.stats): 407 width = 0 408 for func in stat_list: 409 if len(func_std_string(func)) > width: 410 width = len(func_std_string(func)) 411 return width+2, stat_list 412 413 def print_stats(self, *amount): 414 for filename in self.files: 415 print(filename, file=self.stream) 416 if self.files: 417 print(file=self.stream) 418 indent = ' ' * 8 419 for func in self.top_level: 420 print(indent, func_get_function_name(func), file=self.stream) 421 422 print(indent, self.total_calls, "function calls", end=' ', file=self.stream) 423 if self.total_calls != self.prim_calls: 424 print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream) 425 print("in %.3f seconds" % self.total_tt, file=self.stream) 426 print(file=self.stream) 427 width, list = self.get_print_list(amount) 428 if list: 429 self.print_title() 430 for func in list: 431 self.print_line(func) 432 print(file=self.stream) 433 print(file=self.stream) 434 return self 435 436 def print_callees(self, *amount): 437 width, list = self.get_print_list(amount) 438 if list: 439 self.calc_callees() 440 441 self.print_call_heading(width, "called...") 442 for func in list: 443 if func in self.all_callees: 444 self.print_call_line(width, func, self.all_callees[func]) 445 else: 446 self.print_call_line(width, func, {}) 447 print(file=self.stream) 448 print(file=self.stream) 449 return self 450 451 def print_callers(self, *amount): 452 width, list = self.get_print_list(amount) 453 if list: 454 self.print_call_heading(width, "was called by...") 455 for func in list: 456 cc, nc, tt, ct, callers = self.stats[func] 457 self.print_call_line(width, func, callers, "<-") 458 print(file=self.stream) 459 print(file=self.stream) 460 return self 461 462 def print_call_heading(self, name_size, column_title): 463 print("Function ".ljust(name_size) + column_title, file=self.stream) 464 # print sub-header only if we have new-style callers 465 subheader = False 466 for cc, nc, tt, ct, callers in self.stats.values(): 467 if callers: 468 value = next(iter(callers.values())) 469 subheader = isinstance(value, tuple) 470 break 471 if subheader: 472 print(" "*name_size + " ncalls tottime cumtime", file=self.stream) 473 474 def print_call_line(self, name_size, source, call_dict, arrow="->"): 475 print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream) 476 if not call_dict: 477 print(file=self.stream) 478 return 479 clist = sorted(call_dict.keys()) 480 indent = "" 481 for func in clist: 482 name = func_std_string(func) 483 value = call_dict[func] 484 if isinstance(value, tuple): 485 nc, cc, tt, ct = value 486 if nc != cc: 487 substats = '%d/%d' % (nc, cc) 488 else: 489 substats = '%d' % (nc,) 490 substats = '%s %s %s %s' % (substats.rjust(7+2*len(indent)), 491 f8(tt), f8(ct), name) 492 left_width = name_size + 1 493 else: 494 substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3])) 495 left_width = name_size + 3 496 print(indent*left_width + substats, file=self.stream) 497 indent = " " 498 499 def print_title(self): 500 print(' ncalls tottime percall cumtime percall', end=' ', file=self.stream) 501 print('filename:lineno(function)', file=self.stream) 502 503 def print_line(self, func): # hack: should print percentages 504 cc, nc, tt, ct, callers = self.stats[func] 505 c = str(nc) 506 if nc != cc: 507 c = c + '/' + str(cc) 508 print(c.rjust(9), end=' ', file=self.stream) 509 print(f8(tt), end=' ', file=self.stream) 510 if nc == 0: 511 print(' '*8, end=' ', file=self.stream) 512 else: 513 print(f8(tt/nc), end=' ', file=self.stream) 514 print(f8(ct), end=' ', file=self.stream) 515 if cc == 0: 516 print(' '*8, end=' ', file=self.stream) 517 else: 518 print(f8(ct/cc), end=' ', file=self.stream) 519 print(func_std_string(func), file=self.stream) 520 521class TupleComp: 522 """This class provides a generic function for comparing any two tuples. 523 Each instance records a list of tuple-indices (from most significant 524 to least significant), and sort direction (ascending or decending) for 525 each tuple-index. The compare functions can then be used as the function 526 argument to the system sort() function when a list of tuples need to be 527 sorted in the instances order.""" 528 529 def __init__(self, comp_select_list): 530 self.comp_select_list = comp_select_list 531 532 def compare (self, left, right): 533 for index, direction in self.comp_select_list: 534 l = left[index] 535 r = right[index] 536 if l < r: 537 return -direction 538 if l > r: 539 return direction 540 return 0 541 542 543#************************************************************************** 544# func_name is a triple (file:string, line:int, name:string) 545 546def func_strip_path(func_name): 547 filename, line, name = func_name 548 return os.path.basename(filename), line, name 549 550def func_get_function_name(func): 551 return func[2] 552 553def func_std_string(func_name): # match what old profile produced 554 if func_name[:2] == ('~', 0): 555 # special case for built-in functions 556 name = func_name[2] 557 if name.startswith('<') and name.endswith('>'): 558 return '{%s}' % name[1:-1] 559 else: 560 return name 561 else: 562 return "%s:%d(%s)" % func_name 563 564#************************************************************************** 565# The following functions combine statistics for pairs functions. 566# The bulk of the processing involves correctly handling "call" lists, 567# such as callers and callees. 568#************************************************************************** 569 570def add_func_stats(target, source): 571 """Add together all the stats for two profile entries.""" 572 cc, nc, tt, ct, callers = source 573 t_cc, t_nc, t_tt, t_ct, t_callers = target 574 return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, 575 add_callers(t_callers, callers)) 576 577def add_callers(target, source): 578 """Combine two caller lists in a single list.""" 579 new_callers = {} 580 for func, caller in target.items(): 581 new_callers[func] = caller 582 for func, caller in source.items(): 583 if func in new_callers: 584 if isinstance(caller, tuple): 585 # format used by cProfile 586 new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func])) 587 else: 588 # format used by profile 589 new_callers[func] += caller 590 else: 591 new_callers[func] = caller 592 return new_callers 593 594def count_calls(callers): 595 """Sum the caller statistics to get total number of calls received.""" 596 nc = 0 597 for calls in callers.values(): 598 nc += calls 599 return nc 600 601#************************************************************************** 602# The following functions support printing of reports 603#************************************************************************** 604 605def f8(x): 606 return "%8.3f" % x 607 608#************************************************************************** 609# Statistics browser added by ESR, April 2001 610#************************************************************************** 611 612if __name__ == '__main__': 613 import cmd 614 try: 615 import readline 616 except ImportError: 617 pass 618 619 class ProfileBrowser(cmd.Cmd): 620 def __init__(self, profile=None): 621 cmd.Cmd.__init__(self) 622 self.prompt = "% " 623 self.stats = None 624 self.stream = sys.stdout 625 if profile is not None: 626 self.do_read(profile) 627 628 def generic(self, fn, line): 629 args = line.split() 630 processed = [] 631 for term in args: 632 try: 633 processed.append(int(term)) 634 continue 635 except ValueError: 636 pass 637 try: 638 frac = float(term) 639 if frac > 1 or frac < 0: 640 print("Fraction argument must be in [0, 1]", file=self.stream) 641 continue 642 processed.append(frac) 643 continue 644 except ValueError: 645 pass 646 processed.append(term) 647 if self.stats: 648 getattr(self.stats, fn)(*processed) 649 else: 650 print("No statistics object is loaded.", file=self.stream) 651 return 0 652 def generic_help(self): 653 print("Arguments may be:", file=self.stream) 654 print("* An integer maximum number of entries to print.", file=self.stream) 655 print("* A decimal fractional number between 0 and 1, controlling", file=self.stream) 656 print(" what fraction of selected entries to print.", file=self.stream) 657 print("* A regular expression; only entries with function names", file=self.stream) 658 print(" that match it are printed.", file=self.stream) 659 660 def do_add(self, line): 661 if self.stats: 662 try: 663 self.stats.add(line) 664 except OSError as e: 665 print("Failed to load statistics for %s: %s" % (line, e), file=self.stream) 666 else: 667 print("No statistics object is loaded.", file=self.stream) 668 return 0 669 def help_add(self): 670 print("Add profile info from given file to current statistics object.", file=self.stream) 671 672 def do_callees(self, line): 673 return self.generic('print_callees', line) 674 def help_callees(self): 675 print("Print callees statistics from the current stat object.", file=self.stream) 676 self.generic_help() 677 678 def do_callers(self, line): 679 return self.generic('print_callers', line) 680 def help_callers(self): 681 print("Print callers statistics from the current stat object.", file=self.stream) 682 self.generic_help() 683 684 def do_EOF(self, line): 685 print("", file=self.stream) 686 return 1 687 def help_EOF(self): 688 print("Leave the profile browser.", file=self.stream) 689 690 def do_quit(self, line): 691 return 1 692 def help_quit(self): 693 print("Leave the profile browser.", file=self.stream) 694 695 def do_read(self, line): 696 if line: 697 try: 698 self.stats = Stats(line) 699 except OSError as err: 700 print(err.args[1], file=self.stream) 701 return 702 except Exception as err: 703 print(err.__class__.__name__ + ':', err, file=self.stream) 704 return 705 self.prompt = line + "% " 706 elif len(self.prompt) > 2: 707 line = self.prompt[:-2] 708 self.do_read(line) 709 else: 710 print("No statistics object is current -- cannot reload.", file=self.stream) 711 return 0 712 def help_read(self): 713 print("Read in profile data from a specified file.", file=self.stream) 714 print("Without argument, reload the current file.", file=self.stream) 715 716 def do_reverse(self, line): 717 if self.stats: 718 self.stats.reverse_order() 719 else: 720 print("No statistics object is loaded.", file=self.stream) 721 return 0 722 def help_reverse(self): 723 print("Reverse the sort order of the profiling report.", file=self.stream) 724 725 def do_sort(self, line): 726 if not self.stats: 727 print("No statistics object is loaded.", file=self.stream) 728 return 729 abbrevs = self.stats.get_sort_arg_defs() 730 if line and all((x in abbrevs) for x in line.split()): 731 self.stats.sort_stats(*line.split()) 732 else: 733 print("Valid sort keys (unique prefixes are accepted):", file=self.stream) 734 for (key, value) in Stats.sort_arg_dict_default.items(): 735 print("%s -- %s" % (key, value[1]), file=self.stream) 736 return 0 737 def help_sort(self): 738 print("Sort profile data according to specified keys.", file=self.stream) 739 print("(Typing `sort' without arguments lists valid keys.)", file=self.stream) 740 def complete_sort(self, text, *args): 741 return [a for a in Stats.sort_arg_dict_default if a.startswith(text)] 742 743 def do_stats(self, line): 744 return self.generic('print_stats', line) 745 def help_stats(self): 746 print("Print statistics from the current stat object.", file=self.stream) 747 self.generic_help() 748 749 def do_strip(self, line): 750 if self.stats: 751 self.stats.strip_dirs() 752 else: 753 print("No statistics object is loaded.", file=self.stream) 754 def help_strip(self): 755 print("Strip leading path information from filenames in the report.", file=self.stream) 756 757 def help_help(self): 758 print("Show help for a given command.", file=self.stream) 759 760 def postcmd(self, stop, line): 761 if stop: 762 return stop 763 return None 764 765 if len(sys.argv) > 1: 766 initprofile = sys.argv[1] 767 else: 768 initprofile = None 769 try: 770 browser = ProfileBrowser(initprofile) 771 for profile in sys.argv[2:]: 772 browser.do_add(profile) 773 print("Welcome to the profile statistics browser.", file=browser.stream) 774 browser.cmdloop() 775 print("Goodbye.", file=browser.stream) 776 except KeyboardInterrupt: 777 pass 778 779# That's all, folks. 780