From 44b81218a243a7f457023b3224c85054bb8e3eb6 2012-06-11 06:11:44 From: Fernando Perez Date: 2012-06-11 06:11:44 Subject: [PATCH] Merge pull request #1845 from fperez/magic_inspect Fixes to inspection machinery for magics Now that magics are generated via decorators, the inspection machinery wasn't finding data about them such as source code. Fixes that along with some refactoring, cleanup and test additions. Closes gh-1799. --- diff --git a/IPython/core/magics/namespace.py b/IPython/core/magics/namespace.py index b9fa9b1..0ff271f 100644 --- a/IPython/core/magics/namespace.py +++ b/IPython/core/magics/namespace.py @@ -79,7 +79,7 @@ class NamespaceMagics(Magics): In [3]: %pdef urllib.urlopen urllib.urlopen(url, data=None, proxies=None) """ - self._inspect('pdef',parameter_s, namespaces) + self.shell._inspect('pdef',parameter_s, namespaces) @line_magic def pdoc(self, parameter_s='', namespaces=None): @@ -87,12 +87,12 @@ class NamespaceMagics(Magics): If the given object is a class, it will print both the class and the constructor docstrings.""" - self._inspect('pdoc',parameter_s, namespaces) + self.shell._inspect('pdoc',parameter_s, namespaces) @line_magic def psource(self, parameter_s='', namespaces=None): """Print (or run through pager) the source code for an object.""" - self._inspect('psource',parameter_s, namespaces) + self.shell._inspect('psource',parameter_s, namespaces) @line_magic def pfile(self, parameter_s=''): @@ -108,7 +108,7 @@ class NamespaceMagics(Magics): viewer.""" # first interpret argument as an object name - out = self._inspect('pfile',parameter_s) + out = self.shell._inspect('pfile',parameter_s) # if not, try the input as a filename if out == 'not found': try: diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 9ec2244..a94bcf4 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -230,6 +230,75 @@ 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. + """ + # get source if obj was decorated with @decorator + if hasattr(obj, '__wrapped__'): + obj = obj.__wrapped__ + + 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 + 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. + """ + # get source if obj was decorated with @decorator + if hasattr(obj, '__wrapped__'): + obj = obj.__wrapped__ + + 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] + except: + return None + + return lineno + + class Inspector: def __init__(self, color_table=InspectColors, code_color_table=PyColorize.ANSICodeColors, @@ -259,11 +328,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 +340,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 +435,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 +455,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 +629,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 +659,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..8f950de 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 @@ -24,8 +25,10 @@ from IPython.core.magic import (Magics, magics_class, line_magic, cell_magic, line_cell_magic, register_line_magic, register_cell_magic, register_line_cell_magic) +from IPython.external.decorator import decorator from IPython.utils import py3compat + #----------------------------------------------------------------------------- # Globals and constants #----------------------------------------------------------------------------- @@ -37,6 +40,58 @@ 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 = 47 # 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_decorated1(): + + @decorator + def noop1(f): + def wrapper(): + return f(*a, **kw) + return wrapper + + @noop1 + def f(x): + "My docstring" + + nt.assert_equal(oinspect.find_file(f), + os.path.abspath(__file__)) + nt.assert_equal(f.__doc__, "My docstring") + + +def test_find_file_decorated2(): + + @decorator + def noop2(f, *a, **kw): + return f(*a, **kw) + + @noop2 + def f(x): + "My docstring 2" + + nt.assert_equal(oinspect.find_file(f), + os.path.abspath(__file__)) + nt.assert_equal(f.__doc__, "My docstring 2") + + +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):