From f875d435521060469ca03ea267b4121d3c7521ae 2010-09-18 09:55:32 From: Fernando Perez Date: 2010-09-18 09:55:32 Subject: [PATCH] Add function signature info to calltips. 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. --- diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 6787286..51c625c 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1196,9 +1196,10 @@ class InteractiveShell(Configurable, Magic): 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 diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 68ed478..7dd59de 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -76,18 +76,15 @@ info_fields = ['type_name', 'base_class', 'string_form', 'namespace', '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): @@ -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 @@ -178,7 +240,9 @@ class myStringIO(StringIO.StringIO): 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') @@ -565,6 +629,7 @@ class Inspector: ismagic = info.ismagic isalias = info.isalias ospace = info.namespace + # Get docstring, special-casing aliases: if isalias: if not callable(obj): @@ -583,9 +648,8 @@ class Inspector: 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) @@ -650,17 +714,14 @@ class Inspector: 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: @@ -700,11 +761,11 @@ class Inspector: 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 @@ -723,7 +784,7 @@ class Inspector: 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: @@ -735,11 +796,11 @@ class Inspector: 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__) @@ -747,9 +808,30 @@ class Inspector: 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=[], diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py new file mode 100644 index 0000000..d31d442 --- /dev/null +++ b/IPython/core/tests/test_oinspect.py @@ -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)', '') + + +def test_calltip_builtin(): + check_calltip(sum, 'sum', None, sum.__doc__) diff --git a/IPython/frontend/qt/console/call_tip_widget.py b/IPython/frontend/qt/console/call_tip_widget.py index 5e1a248..20f267f 100644 --- a/IPython/frontend/qt/console/call_tip_widget.py +++ b/IPython/frontend/qt/console/call_tip_widget.py @@ -122,15 +122,20 @@ class CallTipWidget(QtGui.QLabel): # '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): diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index e47bab3..2afb8e4 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -10,6 +10,7 @@ from PyQt4 import QtCore, QtGui # 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 @@ -334,9 +335,13 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): 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. diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 1e16ccd..0966e7d 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -303,9 +303,8 @@ class Kernel(Configurable): 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) diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 7134d77..fdb4ec3 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -462,6 +462,9 @@ field names that IPython prints at the terminal. Message type: ``object_info_reply``:: content = { + # The name the object was requested under + 'name' : str, + # Boolean flag indicating whether the named object was found or not. If # it's false, all other fields will be empty. 'found' : bool, @@ -511,7 +514,7 @@ Message type: ``object_info_reply``:: # that these must be matched *in reverse* with the 'args' # list above, since the first positional args have no default # value at all. - func_defaults : list, + defaults : list, }, # For instances, provide the constructor signature (the definition of