Skip to content

Commit

Permalink
Add function signature info to calltips.
Browse files Browse the repository at this point in the history
Also removed custom ObjectInfo namedtuple according to code review
(left as a dict for now, we can make it a list later if really needed).

Added the start of some testing for the object inspector and updated
the messaging spec.
  • Loading branch information
fperez committed Sep 18, 2010
1 parent 878faee commit f875d43
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 42 deletions.
5 changes: 3 additions & 2 deletions IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,9 +1196,10 @@ def _inspect(self, meth, oname, namespaces=None, **kw):
def object_inspect(self, oname):
info = self._object_find(oname)
if info.found:
return self.inspector.info(info.obj, info=info)
return self.inspector.info(info.obj, oname, info=info)
else:
return oinspect.mk_object_info({'found' : False})
return oinspect.mk_object_info({'name' : oname,
'found' : False})

#-------------------------------------------------------------------------
# Things related to history management
Expand Down
134 changes: 108 additions & 26 deletions IPython/core/oinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,15 @@
'call_def', 'call_docstring',
# These won't be printed but will be used to determine how to
# format the object
'ismagic', 'isalias', 'argspec', 'found',
'ismagic', 'isalias', 'argspec', 'found', 'name',
]


ObjectInfo = namedtuple('ObjectInfo', info_fields)


def mk_object_info(kw):
"""Make a f"""
def object_info(**kw):
"""Make an object info dict with all fields present."""
infodict = dict(izip_longest(info_fields, [None]))
infodict.update(kw)
return ObjectInfo(**infodict)
return infodict


def getdoc(obj):
Expand Down Expand Up @@ -161,11 +158,76 @@ def getargspec(obj):
func_obj = obj
elif inspect.ismethod(obj):
func_obj = obj.im_func
elif hasattr(obj, '__call__'):
func_obj = obj.__call__
else:
raise TypeError('arg is not a Python function')
args, varargs, varkw = inspect.getargs(func_obj.func_code)
return args, varargs, varkw, func_obj.func_defaults


def format_argspec(argspec):
"""Format argspect, convenience wrapper around inspect's.
This takes a dict instead of ordered arguments and calls
inspect.format_argspec with the arguments in the necessary order.
"""
return inspect.formatargspec(argspec['args'], argspec['varargs'],
argspec['varkw'], argspec['defaults'])


def call_tip(oinfo, format_call=True):
"""Extract call tip data from an oinfo dict.
Parameters
----------
oinfo : dict
format_call : bool, optional
If True, the call line is formatted and returned as a string. If not, a
tuple of (name, argspec) is returned.
Returns
-------
call_info : None, str or (str, dict) tuple.
When format_call is True, the whole call information is formattted as a
single string. Otherwise, the object's name and its argspec dict are
returned. If no call information is available, None is returned.
docstring : str or None
The most relevant docstring for calling purposes is returned, if
available. The priority is: call docstring for callable instances, then
constructor docstring for classes, then main object's docstring otherwise
(regular functions).
"""
# Get call definition
argspec = oinfo['argspec']
if argspec is None:
call_line = None
else:
# Callable objects will have 'self' as their first argument, prune
# it out if it's there for clarity (since users do *not* pass an
# extra first argument explicitly).
try:
has_self = argspec['args'][0] == 'self'
except (KeyError, IndexError):
pass
else:
if has_self:
argspec['args'] = argspec['args'][1:]

call_line = oinfo['name']+format_argspec(argspec)

# Now get docstring.
# The priority is: call docstring, constructor docstring, main one.
doc = oinfo['call_docstring']
if doc is None:
doc = oinfo['init_docstring']
if doc is None:
doc = oinfo['docstring']

return call_line, doc

#****************************************************************************
# Class definitions

Expand All @@ -178,7 +240,9 @@ def writeln(self,*arg,**kw):


class Inspector:
def __init__(self,color_table,code_color_table,scheme,
def __init__(self, color_table=InspectColors,
code_color_table=PyColorize.ANSICodeColors,
scheme='NoColor',
str_detail_level=0):
self.color_table = color_table
self.parser = PyColorize.Parser(code_color_table,out='str')
Expand Down Expand Up @@ -565,6 +629,7 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
ismagic = info.ismagic
isalias = info.isalias
ospace = info.namespace

# Get docstring, special-casing aliases:
if isalias:
if not callable(obj):
Expand All @@ -583,9 +648,8 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
if formatter is not None:
ds = formatter(ds)

# store output in a dict, we'll later convert it to an ObjectInfo. We
# initialize it here and fill it as we go
out = dict(found=True, isalias=isalias, ismagic=ismagic)
# store output in a dict, we initialize it here and fill it as we go
out = dict(name=oname, found=True, isalias=isalias, ismagic=ismagic)

string_max = 200 # max size of strings to show (snipped if longer)
shalf = int((string_max -5)/2)
Expand Down Expand Up @@ -650,17 +714,14 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
binary_file = True

# reconstruct the function definition and print it:
defln = self._getdef(obj,oname)
defln = self._getdef(obj, oname)
if defln:
out['definition'] = self.format(defln)
args, varargs, varkw, func_defaults = getargspec(obj)
out['argspec'] = dict(args=args, varargs=varargs,
varkw=varkw, func_defaults=func_defaults)


# Docstrings only in detail 0 mode, since source contains them (we
# avoid repetitions). If source fails, we add them back, see below.
if ds and detail_level == 0:
out['docstring'] = indent(ds)
out['docstring'] = ds

# Original source code for any callable
if detail_level:
Expand Down Expand Up @@ -700,11 +761,11 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
if init_def:
out['init_definition'] = self.format(init_def)
if init_ds:
out['init_docstring'] = indent(init_ds)
out['init_docstring'] = init_ds

# and class docstring for instances:
elif obj_type is types.InstanceType or \
isinstance(obj,object):

isinstance(obj, object):
# First, check whether the instance docstring is identical to the
# class one, and print it separately if they don't coincide. In
# most cases they will, but it's nice to print all the info for
Expand All @@ -723,7 +784,7 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
class_ds.startswith('module(name[,') ):
class_ds = None
if class_ds and ds != class_ds:
out['class_docstring'] = indent(class_ds)
out['class_docstring'] = class_ds

# Next, try to show constructor docstrings
try:
Expand All @@ -735,21 +796,42 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
except AttributeError:
init_ds = None
if init_ds:
out['init_docstring'] = indent(init_ds)
out['init_docstring'] = init_ds

# Call form docstring for callable instances
if hasattr(obj,'__call__'):
call_def = self._getdef(obj.__call__,oname)
if hasattr(obj, '__call__'):
call_def = self._getdef(obj.__call__, oname)
if call_def is not None:
out['call_def'] = self.format(call_def)
call_ds = getdoc(obj.__call__)
# Skip Python's auto-generated docstrings
if call_ds and call_ds.startswith('x.__call__(...) <==> x(...)'):
call_ds = None
if call_ds:
out['call_docstring'] = indent(call_ds)
out['call_docstring'] = call_ds

# Compute the object's argspec as a callable. The key is to decide
# whether to pull it from the object itself, from its __init__ or
# from its __call__ method.

if inspect.isclass(obj):
callable_obj = obj.__init__
elif callable(obj):
callable_obj = obj
else:
callable_obj = None

if callable_obj:
try:
args, varargs, varkw, defaults = getargspec(callable_obj)
except (TypeError, AttributeError):
# For extensions/builtins we can't retrieve the argspec
pass
else:
out['argspec'] = dict(args=args, varargs=varargs,
varkw=varkw, defaults=defaults)

return mk_object_info(out)
return object_info(**out)


def psearch(self,pattern,ns_table,ns_search=[],
Expand Down
89 changes: 89 additions & 0 deletions IPython/core/tests/test_oinspect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Tests for the object inspection functionality.
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2010 The IPython Development Team.
#
# Distributed under the terms of the BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from __future__ import print_function

# Stdlib imports

# Third-party imports
import nose.tools as nt

# Our own imports
from .. import oinspect

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

inspector = oinspect.Inspector()

#-----------------------------------------------------------------------------
# Local utilities
#-----------------------------------------------------------------------------

# A few generic objects we can then inspect in the tests below

class Call(object):
"""This is the class docstring."""

def __init__(self, x, y=1):
"""This is the constructor docstring."""

def __call__(self, *a, **kw):
"""This is the call docstring."""

def method(self, x, z=2):
"""Some method's docstring"""

def f(x, y=2, *a, **kw):
"""A simple function."""

def g(y, z=3, *a, **kw):
pass # no docstring


def check_calltip(obj, name, call, docstring):
"""Generic check pattern all calltip tests will use"""
info = inspector.info(obj, name)
call_line, ds = oinspect.call_tip(info)
nt.assert_equal(call_line, call)
nt.assert_equal(ds, docstring)

#-----------------------------------------------------------------------------
# Tests
#-----------------------------------------------------------------------------

def test_calltip_class():
check_calltip(Call, 'Call', 'Call(x, y=1)', Call.__init__.__doc__)


def test_calltip_instance():
c = Call(1)
check_calltip(c, 'c', 'c(*a, **kw)', c.__call__.__doc__)


def test_calltip_method():
c = Call(1)
check_calltip(c.method, 'c.method', 'c.method(x, z=2)', c.method.__doc__)


def test_calltip_function():
check_calltip(f, 'f', 'f(x, y=2, *a, **kw)', f.__doc__)


def test_calltip_function2():
check_calltip(g, 'g', 'g(y, z=3, *a, **kw)', '<no docstring>')


def test_calltip_builtin():
check_calltip(sum, 'sum', None, sum.__doc__)
19 changes: 12 additions & 7 deletions IPython/frontend/qt/console/call_tip_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,20 @@ def showEvent(self, event):
# 'CallTipWidget' interface
#--------------------------------------------------------------------------

def show_docstring(self, doc, maxlines=20):
""" Attempts to show the specified docstring at the current cursor
location. The docstring is dedented and possibly truncated for
def show_call_info(self, call_line=None, doc=None, maxlines=20):
""" Attempts to show the specified call line and docstring at the
current cursor location. The docstring is possibly truncated for
length.
"""
doc = dedent(doc.rstrip()).lstrip()
match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
if match:
doc = doc[:match.end()] + '\n[Documentation continues...]'
if doc:
match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
if match:
doc = doc[:match.end()] + '\n[Documentation continues...]'
else:
doc = ''

if call_line:
doc = '\n\n'.join([call_line, doc])
return self.show_tip(doc)

def show_tip(self, tip):
Expand Down
11 changes: 8 additions & 3 deletions IPython/frontend/qt/console/frontend_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Local imports
from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
from IPython.core.oinspect import call_tip
from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
from IPython.utils.traitlets import Bool
from bracket_matcher import BracketMatcher
Expand Down Expand Up @@ -334,9 +335,13 @@ def _handle_object_info_reply(self, rep):
info = self._request_info.get('call_tip')
if info and info.id == rep['parent_header']['msg_id'] and \
info.pos == cursor.position():
doc = rep['content']['docstring']
if doc:
self._call_tip_widget.show_docstring(doc)
# Get the information for a call tip. For now we format the call
# line as string, later we can pass False to format_call and
# syntax-highlight it ourselves for nicer formatting in the
# calltip.
call_info, doc = call_tip(rep['content'], format_call=True)
if call_info or doc:
self._call_tip_widget.show_call_info(call_info, doc)

def _handle_pyout(self, msg):
""" Handle display hook output.
Expand Down
5 changes: 2 additions & 3 deletions IPython/zmq/ipkernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,8 @@ def complete_request(self, ident, parent):

def object_info_request(self, ident, parent):
object_info = self.shell.object_inspect(parent['content']['oname'])
# Before we send this object over, we turn it into a dict and we scrub
# it for JSON usage
oinfo = json_clean(object_info._asdict())
# Before we send this object over, we scrub it for JSON usage
oinfo = json_clean(object_info)
msg = self.session.send(self.reply_socket, 'object_info_reply',
oinfo, parent, ident)
io.raw_print(msg)
Expand Down
Loading

0 comments on commit f875d43

Please sign in to comment.