1import py 2import sys 3from inspect import CO_VARARGS, CO_VARKEYWORDS, isclass 4 5builtin_repr = repr 6 7reprlib = py.builtin._tryimport('repr', 'reprlib') 8 9if sys.version_info[0] >= 3: 10 from traceback import format_exception_only 11else: 12 from py._code._py2traceback import format_exception_only 13 14import traceback 15 16 17class Code(object): 18 """ wrapper around Python code objects """ 19 def __init__(self, rawcode): 20 if not hasattr(rawcode, "co_filename"): 21 rawcode = py.code.getrawcode(rawcode) 22 try: 23 self.filename = rawcode.co_filename 24 self.firstlineno = rawcode.co_firstlineno - 1 25 self.name = rawcode.co_name 26 except AttributeError: 27 raise TypeError("not a code object: %r" % (rawcode,)) 28 self.raw = rawcode 29 30 def __eq__(self, other): 31 return self.raw == other.raw 32 33 def __ne__(self, other): 34 return not self == other 35 36 @property 37 def path(self): 38 """ return a path object pointing to source code (note that it 39 might not point to an actually existing file). """ 40 p = py.path.local(self.raw.co_filename) 41 # maybe don't try this checking 42 if not p.check(): 43 # XXX maybe try harder like the weird logic 44 # in the standard lib [linecache.updatecache] does? 45 p = self.raw.co_filename 46 return p 47 48 @property 49 def fullsource(self): 50 """ return a py.code.Source object for the full source file of the code 51 """ 52 from py._code import source 53 full, _ = source.findsource(self.raw) 54 return full 55 56 def source(self): 57 """ return a py.code.Source object for the code object's source only 58 """ 59 # return source only for that part of code 60 return py.code.Source(self.raw) 61 62 def getargs(self, var=False): 63 """ return a tuple with the argument names for the code object 64 65 if 'var' is set True also return the names of the variable and 66 keyword arguments when present 67 """ 68 # handfull shortcut for getting args 69 raw = self.raw 70 argcount = raw.co_argcount 71 if var: 72 argcount += raw.co_flags & CO_VARARGS 73 argcount += raw.co_flags & CO_VARKEYWORDS 74 return raw.co_varnames[:argcount] 75 76class Frame(object): 77 """Wrapper around a Python frame holding f_locals and f_globals 78 in which expressions can be evaluated.""" 79 80 def __init__(self, frame): 81 self.lineno = frame.f_lineno - 1 82 self.f_globals = frame.f_globals 83 self.f_locals = frame.f_locals 84 self.raw = frame 85 self.code = py.code.Code(frame.f_code) 86 87 @property 88 def statement(self): 89 """ statement this frame is at """ 90 if self.code.fullsource is None: 91 return py.code.Source("") 92 return self.code.fullsource.getstatement(self.lineno) 93 94 def eval(self, code, **vars): 95 """ evaluate 'code' in the frame 96 97 'vars' are optional additional local variables 98 99 returns the result of the evaluation 100 """ 101 f_locals = self.f_locals.copy() 102 f_locals.update(vars) 103 return eval(code, self.f_globals, f_locals) 104 105 def exec_(self, code, **vars): 106 """ exec 'code' in the frame 107 108 'vars' are optiona; additional local variables 109 """ 110 f_locals = self.f_locals.copy() 111 f_locals.update(vars) 112 py.builtin.exec_(code, self.f_globals, f_locals) 113 114 def repr(self, object): 115 """ return a 'safe' (non-recursive, one-line) string repr for 'object' 116 """ 117 return py.io.saferepr(object) 118 119 def is_true(self, object): 120 return object 121 122 def getargs(self, var=False): 123 """ return a list of tuples (name, value) for all arguments 124 125 if 'var' is set True also include the variable and keyword 126 arguments when present 127 """ 128 retval = [] 129 for arg in self.code.getargs(var): 130 try: 131 retval.append((arg, self.f_locals[arg])) 132 except KeyError: 133 pass # this can occur when using Psyco 134 return retval 135 136 137class TracebackEntry(object): 138 """ a single entry in a traceback """ 139 140 _repr_style = None 141 exprinfo = None 142 143 def __init__(self, rawentry): 144 self._rawentry = rawentry 145 self.lineno = rawentry.tb_lineno - 1 146 147 def set_repr_style(self, mode): 148 assert mode in ("short", "long") 149 self._repr_style = mode 150 151 @property 152 def frame(self): 153 return py.code.Frame(self._rawentry.tb_frame) 154 155 @property 156 def relline(self): 157 return self.lineno - self.frame.code.firstlineno 158 159 def __repr__(self): 160 return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno+1) 161 162 @property 163 def statement(self): 164 """ py.code.Source object for the current statement """ 165 source = self.frame.code.fullsource 166 return source.getstatement(self.lineno) 167 168 @property 169 def path(self): 170 """ path to the source code """ 171 return self.frame.code.path 172 173 def getlocals(self): 174 return self.frame.f_locals 175 locals = property(getlocals, None, None, "locals of underlaying frame") 176 177 def reinterpret(self): 178 """Reinterpret the failing statement and returns a detailed information 179 about what operations are performed.""" 180 if self.exprinfo is None: 181 source = str(self.statement).strip() 182 x = py.code._reinterpret(source, self.frame, should_fail=True) 183 if not isinstance(x, str): 184 raise TypeError("interpret returned non-string %r" % (x,)) 185 self.exprinfo = x 186 return self.exprinfo 187 188 def getfirstlinesource(self): 189 # on Jython this firstlineno can be -1 apparently 190 return max(self.frame.code.firstlineno, 0) 191 192 def getsource(self, astcache=None): 193 """ return failing source code. """ 194 # we use the passed in astcache to not reparse asttrees 195 # within exception info printing 196 from py._code.source import getstatementrange_ast 197 source = self.frame.code.fullsource 198 if source is None: 199 return None 200 key = astnode = None 201 if astcache is not None: 202 key = self.frame.code.path 203 if key is not None: 204 astnode = astcache.get(key, None) 205 start = self.getfirstlinesource() 206 try: 207 astnode, _, end = getstatementrange_ast(self.lineno, source, 208 astnode=astnode) 209 except SyntaxError: 210 end = self.lineno + 1 211 else: 212 if key is not None: 213 astcache[key] = astnode 214 return source[start:end] 215 216 source = property(getsource) 217 218 def ishidden(self): 219 """ return True if the current frame has a var __tracebackhide__ 220 resolving to True 221 222 mostly for internal use 223 """ 224 try: 225 return self.frame.f_locals['__tracebackhide__'] 226 except KeyError: 227 try: 228 return self.frame.f_globals['__tracebackhide__'] 229 except KeyError: 230 return False 231 232 def __str__(self): 233 try: 234 fn = str(self.path) 235 except py.error.Error: 236 fn = '???' 237 name = self.frame.code.name 238 try: 239 line = str(self.statement).lstrip() 240 except KeyboardInterrupt: 241 raise 242 except: 243 line = "???" 244 return " File %r:%d in %s\n %s\n" % (fn, self.lineno+1, name, line) 245 246 def name(self): 247 return self.frame.code.raw.co_name 248 name = property(name, None, None, "co_name of underlaying code") 249 250 251class Traceback(list): 252 """ Traceback objects encapsulate and offer higher level 253 access to Traceback entries. 254 """ 255 Entry = TracebackEntry 256 257 def __init__(self, tb): 258 """ initialize from given python traceback object. """ 259 if hasattr(tb, 'tb_next'): 260 def f(cur): 261 while cur is not None: 262 yield self.Entry(cur) 263 cur = cur.tb_next 264 list.__init__(self, f(tb)) 265 else: 266 list.__init__(self, tb) 267 268 def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): 269 """ return a Traceback instance wrapping part of this Traceback 270 271 by provding any combination of path, lineno and firstlineno, the 272 first frame to start the to-be-returned traceback is determined 273 274 this allows cutting the first part of a Traceback instance e.g. 275 for formatting reasons (removing some uninteresting bits that deal 276 with handling of the exception/traceback) 277 """ 278 for x in self: 279 code = x.frame.code 280 codepath = code.path 281 if ((path is None or codepath == path) and 282 (excludepath is None or not hasattr(codepath, 'relto') or 283 not codepath.relto(excludepath)) and 284 (lineno is None or x.lineno == lineno) and 285 (firstlineno is None or x.frame.code.firstlineno == firstlineno)): 286 return Traceback(x._rawentry) 287 return self 288 289 def __getitem__(self, key): 290 val = super(Traceback, self).__getitem__(key) 291 if isinstance(key, type(slice(0))): 292 val = self.__class__(val) 293 return val 294 295 def filter(self, fn=lambda x: not x.ishidden()): 296 """ return a Traceback instance with certain items removed 297 298 fn is a function that gets a single argument, a TracebackItem 299 instance, and should return True when the item should be added 300 to the Traceback, False when not 301 302 by default this removes all the TracebackItems which are hidden 303 (see ishidden() above) 304 """ 305 return Traceback(filter(fn, self)) 306 307 def getcrashentry(self): 308 """ return last non-hidden traceback entry that lead 309 to the exception of a traceback. 310 """ 311 for i in range(-1, -len(self)-1, -1): 312 entry = self[i] 313 if not entry.ishidden(): 314 return entry 315 return self[-1] 316 317 def recursionindex(self): 318 """ return the index of the frame/TracebackItem where recursion 319 originates if appropriate, None if no recursion occurred 320 """ 321 cache = {} 322 for i, entry in enumerate(self): 323 # id for the code.raw is needed to work around 324 # the strange metaprogramming in the decorator lib from pypi 325 # which generates code objects that have hash/value equality 326 #XXX needs a test 327 key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno 328 #print "checking for recursion at", key 329 l = cache.setdefault(key, []) 330 if l: 331 f = entry.frame 332 loc = f.f_locals 333 for otherloc in l: 334 if f.is_true(f.eval(co_equal, 335 __recursioncache_locals_1=loc, 336 __recursioncache_locals_2=otherloc)): 337 return i 338 l.append(entry.frame.f_locals) 339 return None 340 341co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', 342 '?', 'eval') 343 344class ExceptionInfo(object): 345 """ wraps sys.exc_info() objects and offers 346 help for navigating the traceback. 347 """ 348 _striptext = '' 349 def __init__(self, tup=None, exprinfo=None): 350 if tup is None: 351 tup = sys.exc_info() 352 if exprinfo is None and isinstance(tup[1], AssertionError): 353 exprinfo = getattr(tup[1], 'msg', None) 354 if exprinfo is None: 355 exprinfo = str(tup[1]) 356 if exprinfo and exprinfo.startswith('assert '): 357 self._striptext = 'AssertionError: ' 358 self._excinfo = tup 359 #: the exception class 360 self.type = tup[0] 361 #: the exception instance 362 self.value = tup[1] 363 #: the exception raw traceback 364 self.tb = tup[2] 365 #: the exception type name 366 self.typename = self.type.__name__ 367 #: the exception traceback (py.code.Traceback instance) 368 self.traceback = py.code.Traceback(self.tb) 369 370 def __repr__(self): 371 return "<ExceptionInfo %s tblen=%d>" % ( 372 self.typename, len(self.traceback)) 373 374 def exconly(self, tryshort=False): 375 """ return the exception as a string 376 377 when 'tryshort' resolves to True, and the exception is a 378 py.code._AssertionError, only the actual exception part of 379 the exception representation is returned (so 'AssertionError: ' is 380 removed from the beginning) 381 """ 382 lines = format_exception_only(self.type, self.value) 383 text = ''.join(lines) 384 text = text.rstrip() 385 if tryshort: 386 if text.startswith(self._striptext): 387 text = text[len(self._striptext):] 388 return text 389 390 def errisinstance(self, exc): 391 """ return True if the exception is an instance of exc """ 392 return isinstance(self.value, exc) 393 394 def _getreprcrash(self): 395 exconly = self.exconly(tryshort=True) 396 entry = self.traceback.getcrashentry() 397 path, lineno = entry.frame.code.raw.co_filename, entry.lineno 398 return ReprFileLocation(path, lineno+1, exconly) 399 400 def getrepr(self, showlocals=False, style="long", 401 abspath=False, tbfilter=True, funcargs=False): 402 """ return str()able representation of this exception info. 403 showlocals: show locals per traceback entry 404 style: long|short|no|native traceback style 405 tbfilter: hide entries (where __tracebackhide__ is true) 406 407 in case of style==native, tbfilter and showlocals is ignored. 408 """ 409 if style == 'native': 410 return ReprExceptionInfo(ReprTracebackNative( 411 traceback.format_exception( 412 self.type, 413 self.value, 414 self.traceback[0]._rawentry, 415 )), self._getreprcrash()) 416 417 fmt = FormattedExcinfo( 418 showlocals=showlocals, style=style, 419 abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) 420 return fmt.repr_excinfo(self) 421 422 def __str__(self): 423 entry = self.traceback[-1] 424 loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) 425 return str(loc) 426 427 def __unicode__(self): 428 entry = self.traceback[-1] 429 loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) 430 return loc.__unicode__() 431 432 433class FormattedExcinfo(object): 434 """ presenting information about failing Functions and Generators. """ 435 # for traceback entries 436 flow_marker = ">" 437 fail_marker = "E" 438 439 def __init__(self, showlocals=False, style="long", 440 abspath=True, tbfilter=True, funcargs=False): 441 self.showlocals = showlocals 442 self.style = style 443 self.tbfilter = tbfilter 444 self.funcargs = funcargs 445 self.abspath = abspath 446 self.astcache = {} 447 448 def _getindent(self, source): 449 # figure out indent for given source 450 try: 451 s = str(source.getstatement(len(source)-1)) 452 except KeyboardInterrupt: 453 raise 454 except: 455 try: 456 s = str(source[-1]) 457 except KeyboardInterrupt: 458 raise 459 except: 460 return 0 461 return 4 + (len(s) - len(s.lstrip())) 462 463 def _getentrysource(self, entry): 464 source = entry.getsource(self.astcache) 465 if source is not None: 466 source = source.deindent() 467 return source 468 469 def _saferepr(self, obj): 470 return py.io.saferepr(obj) 471 472 def repr_args(self, entry): 473 if self.funcargs: 474 args = [] 475 for argname, argvalue in entry.frame.getargs(var=True): 476 args.append((argname, self._saferepr(argvalue))) 477 return ReprFuncArgs(args) 478 479 def get_source(self, source, line_index=-1, excinfo=None, short=False): 480 """ return formatted and marked up source lines. """ 481 lines = [] 482 if source is None or line_index >= len(source.lines): 483 source = py.code.Source("???") 484 line_index = 0 485 if line_index < 0: 486 line_index += len(source) 487 space_prefix = " " 488 if short: 489 lines.append(space_prefix + source.lines[line_index].strip()) 490 else: 491 for line in source.lines[:line_index]: 492 lines.append(space_prefix + line) 493 lines.append(self.flow_marker + " " + source.lines[line_index]) 494 for line in source.lines[line_index+1:]: 495 lines.append(space_prefix + line) 496 if excinfo is not None: 497 indent = 4 if short else self._getindent(source) 498 lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) 499 return lines 500 501 def get_exconly(self, excinfo, indent=4, markall=False): 502 lines = [] 503 indent = " " * indent 504 # get the real exception information out 505 exlines = excinfo.exconly(tryshort=True).split('\n') 506 failindent = self.fail_marker + indent[1:] 507 for line in exlines: 508 lines.append(failindent + line) 509 if not markall: 510 failindent = indent 511 return lines 512 513 def repr_locals(self, locals): 514 if self.showlocals: 515 lines = [] 516 keys = [loc for loc in locals if loc[0] != "@"] 517 keys.sort() 518 for name in keys: 519 value = locals[name] 520 if name == '__builtins__': 521 lines.append("__builtins__ = <builtins>") 522 else: 523 # This formatting could all be handled by the 524 # _repr() function, which is only reprlib.Repr in 525 # disguise, so is very configurable. 526 str_repr = self._saferepr(value) 527 #if len(str_repr) < 70 or not isinstance(value, 528 # (list, tuple, dict)): 529 lines.append("%-10s = %s" %(name, str_repr)) 530 #else: 531 # self._line("%-10s =\\" % (name,)) 532 # # XXX 533 # pprint.pprint(value, stream=self.excinfowriter) 534 return ReprLocals(lines) 535 536 def repr_traceback_entry(self, entry, excinfo=None): 537 source = self._getentrysource(entry) 538 if source is None: 539 source = py.code.Source("???") 540 line_index = 0 541 else: 542 # entry.getfirstlinesource() can be -1, should be 0 on jython 543 line_index = entry.lineno - max(entry.getfirstlinesource(), 0) 544 545 lines = [] 546 style = entry._repr_style 547 if style is None: 548 style = self.style 549 if style in ("short", "long"): 550 short = style == "short" 551 reprargs = self.repr_args(entry) if not short else None 552 s = self.get_source(source, line_index, excinfo, short=short) 553 lines.extend(s) 554 if short: 555 message = "in %s" %(entry.name) 556 else: 557 message = excinfo and excinfo.typename or "" 558 path = self._makepath(entry.path) 559 filelocrepr = ReprFileLocation(path, entry.lineno+1, message) 560 localsrepr = None 561 if not short: 562 localsrepr = self.repr_locals(entry.locals) 563 return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) 564 if excinfo: 565 lines.extend(self.get_exconly(excinfo, indent=4)) 566 return ReprEntry(lines, None, None, None, style) 567 568 def _makepath(self, path): 569 if not self.abspath: 570 try: 571 np = py.path.local().bestrelpath(path) 572 except OSError: 573 return path 574 if len(np) < len(str(path)): 575 path = np 576 return path 577 578 def repr_traceback(self, excinfo): 579 traceback = excinfo.traceback 580 if self.tbfilter: 581 traceback = traceback.filter() 582 recursionindex = None 583 if excinfo.errisinstance(RuntimeError): 584 if "maximum recursion depth exceeded" in str(excinfo.value): 585 recursionindex = traceback.recursionindex() 586 last = traceback[-1] 587 entries = [] 588 extraline = None 589 for index, entry in enumerate(traceback): 590 einfo = (last == entry) and excinfo or None 591 reprentry = self.repr_traceback_entry(entry, einfo) 592 entries.append(reprentry) 593 if index == recursionindex: 594 extraline = "!!! Recursion detected (same locals & position)" 595 break 596 return ReprTraceback(entries, extraline, style=self.style) 597 598 def repr_excinfo(self, excinfo): 599 reprtraceback = self.repr_traceback(excinfo) 600 reprcrash = excinfo._getreprcrash() 601 return ReprExceptionInfo(reprtraceback, reprcrash) 602 603class TerminalRepr: 604 def __str__(self): 605 s = self.__unicode__() 606 if sys.version_info[0] < 3: 607 s = s.encode('utf-8') 608 return s 609 610 def __unicode__(self): 611 # FYI this is called from pytest-xdist's serialization of exception 612 # information. 613 io = py.io.TextIO() 614 tw = py.io.TerminalWriter(file=io) 615 self.toterminal(tw) 616 return io.getvalue().strip() 617 618 def __repr__(self): 619 return "<%s instance at %0x>" %(self.__class__, id(self)) 620 621 622class ReprExceptionInfo(TerminalRepr): 623 def __init__(self, reprtraceback, reprcrash): 624 self.reprtraceback = reprtraceback 625 self.reprcrash = reprcrash 626 self.sections = [] 627 628 def addsection(self, name, content, sep="-"): 629 self.sections.append((name, content, sep)) 630 631 def toterminal(self, tw): 632 self.reprtraceback.toterminal(tw) 633 for name, content, sep in self.sections: 634 tw.sep(sep, name) 635 tw.line(content) 636 637class ReprTraceback(TerminalRepr): 638 entrysep = "_ " 639 640 def __init__(self, reprentries, extraline, style): 641 self.reprentries = reprentries 642 self.extraline = extraline 643 self.style = style 644 645 def toterminal(self, tw): 646 # the entries might have different styles 647 last_style = None 648 for i, entry in enumerate(self.reprentries): 649 if entry.style == "long": 650 tw.line("") 651 entry.toterminal(tw) 652 if i < len(self.reprentries) - 1: 653 next_entry = self.reprentries[i+1] 654 if entry.style == "long" or \ 655 entry.style == "short" and next_entry.style == "long": 656 tw.sep(self.entrysep) 657 658 if self.extraline: 659 tw.line(self.extraline) 660 661class ReprTracebackNative(ReprTraceback): 662 def __init__(self, tblines): 663 self.style = "native" 664 self.reprentries = [ReprEntryNative(tblines)] 665 self.extraline = None 666 667class ReprEntryNative(TerminalRepr): 668 style = "native" 669 670 def __init__(self, tblines): 671 self.lines = tblines 672 673 def toterminal(self, tw): 674 tw.write("".join(self.lines)) 675 676class ReprEntry(TerminalRepr): 677 localssep = "_ " 678 679 def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): 680 self.lines = lines 681 self.reprfuncargs = reprfuncargs 682 self.reprlocals = reprlocals 683 self.reprfileloc = filelocrepr 684 self.style = style 685 686 def toterminal(self, tw): 687 if self.style == "short": 688 self.reprfileloc.toterminal(tw) 689 for line in self.lines: 690 red = line.startswith("E ") 691 tw.line(line, bold=True, red=red) 692 #tw.line("") 693 return 694 if self.reprfuncargs: 695 self.reprfuncargs.toterminal(tw) 696 for line in self.lines: 697 red = line.startswith("E ") 698 tw.line(line, bold=True, red=red) 699 if self.reprlocals: 700 #tw.sep(self.localssep, "Locals") 701 tw.line("") 702 self.reprlocals.toterminal(tw) 703 if self.reprfileloc: 704 if self.lines: 705 tw.line("") 706 self.reprfileloc.toterminal(tw) 707 708 def __str__(self): 709 return "%s\n%s\n%s" % ("\n".join(self.lines), 710 self.reprlocals, 711 self.reprfileloc) 712 713class ReprFileLocation(TerminalRepr): 714 def __init__(self, path, lineno, message): 715 self.path = str(path) 716 self.lineno = lineno 717 self.message = message 718 719 def toterminal(self, tw): 720 # filename and lineno output for each entry, 721 # using an output format that most editors unterstand 722 msg = self.message 723 i = msg.find("\n") 724 if i != -1: 725 msg = msg[:i] 726 tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) 727 728class ReprLocals(TerminalRepr): 729 def __init__(self, lines): 730 self.lines = lines 731 732 def toterminal(self, tw): 733 for line in self.lines: 734 tw.line(line) 735 736class ReprFuncArgs(TerminalRepr): 737 def __init__(self, args): 738 self.args = args 739 740 def toterminal(self, tw): 741 if self.args: 742 linesofar = "" 743 for name, value in self.args: 744 ns = "%s = %s" %(name, value) 745 if len(ns) + len(linesofar) + 2 > tw.fullwidth: 746 if linesofar: 747 tw.line(linesofar) 748 linesofar = ns 749 else: 750 if linesofar: 751 linesofar += ", " + ns 752 else: 753 linesofar = ns 754 if linesofar: 755 tw.line(linesofar) 756 tw.line("") 757 758 759 760oldbuiltins = {} 761 762def patch_builtins(assertion=True, compile=True): 763 """ put compile and AssertionError builtins to Python's builtins. """ 764 if assertion: 765 from py._code import assertion 766 l = oldbuiltins.setdefault('AssertionError', []) 767 l.append(py.builtin.builtins.AssertionError) 768 py.builtin.builtins.AssertionError = assertion.AssertionError 769 if compile: 770 l = oldbuiltins.setdefault('compile', []) 771 l.append(py.builtin.builtins.compile) 772 py.builtin.builtins.compile = py.code.compile 773 774def unpatch_builtins(assertion=True, compile=True): 775 """ remove compile and AssertionError builtins from Python builtins. """ 776 if assertion: 777 py.builtin.builtins.AssertionError = oldbuiltins['AssertionError'].pop() 778 if compile: 779 py.builtin.builtins.compile = oldbuiltins['compile'].pop() 780 781def getrawcode(obj, trycall=True): 782 """ return code object for given function. """ 783 try: 784 return obj.__code__ 785 except AttributeError: 786 obj = getattr(obj, 'im_func', obj) 787 obj = getattr(obj, 'func_code', obj) 788 obj = getattr(obj, 'f_code', obj) 789 obj = getattr(obj, '__code__', obj) 790 if trycall and not hasattr(obj, 'co_firstlineno'): 791 if hasattr(obj, '__call__') and not isclass(obj): 792 x = getrawcode(obj.__call__, trycall=False) 793 if hasattr(x, 'co_firstlineno'): 794 return x 795 return obj 796 797