Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes to inspection machinery for magics #1845

Merged
merged 4 commits into from
Jun 11, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions IPython/core/magics/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,20 @@ def pdef(self, parameter_s='', namespaces=None):
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):
"""Print the docstring for an object.

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=''):
Expand All @@ -108,7 +108,7 @@ def pfile(self, parameter_s=''):
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:
Expand Down
135 changes: 94 additions & 41 deletions IPython/core/oinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -259,19 +328,19 @@ def __head(self,h):
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:
print 'for %s' % oname
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."""
Expand Down Expand Up @@ -366,28 +435,18 @@ def psource(self,obj,oname=''):
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):
Expand All @@ -396,7 +455,7 @@ def pfile(self,obj,oname=''):
# 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.
Expand Down Expand Up @@ -570,24 +629,18 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):

# 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('<string>'):
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('<string>'):
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:
Expand All @@ -606,10 +659,10 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
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:
Expand Down
55 changes: 55 additions & 0 deletions IPython/core/tests/test_oinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from __future__ import print_function

# Stdlib imports
import os

# Third-party imports
import nose.tools as nt
Expand All @@ -24,8 +25,10 @@
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
#-----------------------------------------------------------------------------
Expand All @@ -37,6 +40,58 @@
# 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):
Expand Down