From 45d1949a8ad2abc6d31be6250c760096ccedd0fe 2012-06-04 04:10:45 From: Fernando Perez Date: 2012-06-04 04:10:45 Subject: [PATCH] Fix finding of file info for magics and decorated functions. Refactored some of the oinspect logic into standalone functions and added tests. --- diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 9ec2244..33514ab 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -230,6 +230,77 @@ def call_tip(oinfo, format_call=True): return call_line, doc +def find_file(obj): + """Find the absolute path to the file where an object was defined. + + This is essentially a robust wrapper around `inspect.getabsfile`. + + Returns None if no file can be found. + + Parameters + ---------- + obj : any Python object + + Returns + ------- + fname : str + The absolute path to the file where the object was defined. + """ + try: + fname = inspect.getabsfile(obj) + except TypeError: + # For an instance, the file that matters is where its class was + # declared. + if hasattr(obj,'__class__'): + try: + fname = inspect.getabsfile(obj.__class__) + except TypeError: + # Can happen for builtins + fname = None + except: + fname = None + else: + if fname.endswith('') and hasattr(obj, '__wrapped__'): + # Analyze decorated functions and methods correctly + fname = inspect.getabsfile(obj.__wrapped__) + return fname + + +def find_source_lines(obj): + """Find the line number in a file where an object was defined. + + This is essentially a robust wrapper around `inspect.getsourcelines`. + + Returns None if no file can be found. + + Parameters + ---------- + obj : any Python object + + Returns + ------- + lineno : int + The line number where the object definition starts. + """ + try: + try: + lineno = inspect.getsourcelines(obj)[1] + except TypeError: + # For instances, try the class object like getsource() does + if hasattr(obj,'__class__'): + lineno = inspect.getsourcelines(obj.__class__)[1] + # Adjust the inspected object so getabsfile() below works + obj = obj.__class__ + except IOError: + if hasattr(obj, '__wrapped__'): + obj = obj.__wrapped__ + lineno = inspect.getsourcelines(obj)[1] + except: + return None + + return lineno + + class Inspector: def __init__(self, color_table=InspectColors, code_color_table=PyColorize.ANSICodeColors, @@ -259,11 +330,11 @@ class Inspector: return '%s%s%s' % (self.color_table.active_colors.header,h, self.color_table.active_colors.normal) - def set_active_scheme(self,scheme): + def set_active_scheme(self, scheme): self.color_table.set_active_scheme(scheme) self.parser.color_table.set_active_scheme(scheme) - def noinfo(self,msg,oname): + def noinfo(self, msg, oname): """Generic message when no information is found.""" print 'No %s found' % msg, if oname: @@ -271,7 +342,7 @@ class Inspector: else: print - def pdef(self,obj,oname=''): + def pdef(self, obj, oname=''): """Print the definition header for any callable object. If the object is a class, print the constructor information.""" @@ -366,28 +437,18 @@ class Inspector: else: page.page(self.format(py3compat.unicode_to_str(src))) - def pfile(self,obj,oname=''): + def pfile(self, obj, oname=''): """Show the whole file where an object was defined.""" - - try: - try: - lineno = inspect.getsourcelines(obj)[1] - except TypeError: - # For instances, try the class object like getsource() does - if hasattr(obj,'__class__'): - lineno = inspect.getsourcelines(obj.__class__)[1] - # Adjust the inspected object so getabsfile() below works - obj = obj.__class__ - except: - self.noinfo('file',oname) + + lineno = find_source_lines(obj) + if lineno is None: + self.noinfo('file', oname) return - # We only reach this point if object was successfully queried - - # run contents of file through pager starting at line - # where the object is defined - ofile = inspect.getabsfile(obj) - + ofile = find_file(obj) + # run contents of file through pager starting at line where the object + # is defined, as long as the file isn't binary and is actually on the + # filesystem. if ofile.endswith(('.so', '.dll', '.pyd')): print 'File %r is binary, not printing.' % ofile elif not os.path.isfile(ofile): @@ -396,7 +457,7 @@ class Inspector: # Print only text files, not extension binaries. Note that # getsourcelines returns lineno with 1-offset and page() uses # 0-offset, so we must adjust. - page.page(self.format(open(ofile).read()),lineno-1) + page.page(self.format(open(ofile).read()), lineno-1) def _format_fields(self, fields, title_width=12): """Formats a list of fields for display. @@ -570,24 +631,18 @@ class Inspector: # Filename where object was defined binary_file = False - try: - try: - fname = inspect.getabsfile(obj) - except TypeError: - # For an instance, the file that matters is where its class was - # declared. - if hasattr(obj,'__class__'): - fname = inspect.getabsfile(obj.__class__) - if fname.endswith(''): - fname = 'Dynamically generated function. No source code available.' - if fname.endswith(('.so', '.dll', '.pyd')): - binary_file = True - out['file'] = fname - except: + fname = find_file(obj) + if fname is None: # if anything goes wrong, we don't want to show source, so it's as # if the file was binary binary_file = True - + else: + if fname.endswith(('.so', '.dll', '.pyd')): + binary_file = True + elif fname.endswith(''): + fname = 'Dynamically generated function. No source code available.' + out['file'] = fname + # reconstruct the function definition and print it: defln = self._getdef(obj, oname) if defln: @@ -606,10 +661,10 @@ class Inspector: source = None try: try: - source = getsource(obj,binary_file) + source = getsource(obj, binary_file) except TypeError: - if hasattr(obj,'__class__'): - source = getsource(obj.__class__,binary_file) + if hasattr(obj, '__class__'): + source = getsource(obj.__class__, binary_file) if source is not None: out['source'] = source.rstrip() except Exception: diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index bfdfc47..62b992c 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -14,6 +14,7 @@ from __future__ import print_function # Stdlib imports +import os # Third-party imports import nose.tools as nt @@ -37,6 +38,26 @@ ip = get_ipython() # Local utilities #----------------------------------------------------------------------------- +# WARNING: since this test checks the line number where a function is +# defined, if any code is inserted above, the following line will need to be +# updated. Do NOT insert any whitespace between the next line and the function +# definition below. +THIS_LINE_NUMBER = 45 # Put here the actual number of this line +def test_find_source_lines(): + nt.assert_equal(oinspect.find_source_lines(test_find_source_lines), + THIS_LINE_NUMBER+1) + + +def test_find_file(): + nt.assert_equal(oinspect.find_file(test_find_file), + os.path.abspath(__file__)) + + +def test_find_file_magic(): + run = ip.find_line_magic('run') + nt.assert_not_equal(oinspect.find_file(run), None) + + # A few generic objects we can then inspect in the tests below class Call(object):