Skip to content

Commit

Permalink
Show signature with Jedi.
Browse files Browse the repository at this point in the history
When completion are function, jedi is capable of giving us the function
signature. This adds a signature field to the (still unstable) completer
API and make use of it in the IPython terminal UI.

It is not (yet) exposed by the ipykernel.

Additionally add typing to a couple of locations.
  • Loading branch information
Carreau committed Jun 5, 2017
1 parent 57cf9b1 commit 6992964
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 19 deletions.
88 changes: 76 additions & 12 deletions IPython/core/completer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# encoding: utf-8
"""Completion for IPython.
This module started as fork of the rlcompleter module in the Python standard
Expand Down Expand Up @@ -87,7 +86,7 @@
module in debug mode (start IPython with ``--Completer.debug=True``) in order
to have extra logging information is :any:`jedi` is crashing, or if current
IPython completer pending deprecations are returning results not yet handled
by :any:`jedi`.
by :any:`jedi`
Using Jedi for tab completion allow snippets like the following to work without
having to execute any code:
Expand All @@ -103,8 +102,6 @@
current development version to get better completions.
"""

# skip module docstests
skip_doctest = True

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
Expand Down Expand Up @@ -142,9 +139,13 @@
from IPython.utils.process import arg_split
from traitlets import Bool, Enum, observe, Int

# skip module docstests
skip_doctest = True

try:
import jedi
import jedi.api.helpers
import jedi.api.classes
JEDI_INSTALLED = True
except ImportError:
JEDI_INSTALLED = False
Expand Down Expand Up @@ -237,7 +238,7 @@ def protect_filename(s, protectables=PROTECTABLES):
return s


def expand_user(path):
def expand_user(path:str) -> Tuple[str, bool, str]:
"""Expand ``~``-style usernames in strings.
This is similar to :func:`os.path.expanduser`, but it computes and returns
Expand Down Expand Up @@ -277,7 +278,7 @@ def expand_user(path):
return newpath, tilde_expand, tilde_val


def compress_user(path, tilde_expand, tilde_val):
def compress_user(path:str, tilde_expand:bool, tilde_val:str) -> str:
"""Does the opposite of expand_user, with its outputs.
"""
if tilde_expand:
Expand Down Expand Up @@ -338,6 +339,8 @@ def __init__(self, name):
self.complete = name
self.type = 'crashed'
self.name_with_symbols = name
self.signature = ''
self._origin = 'fake'

def __repr__(self):
return '<Fake completion object jedi has crashed>'
Expand Down Expand Up @@ -366,7 +369,9 @@ class Completion:
``IPython.python_matches``, ``IPython.magics_matches``...).
"""

def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='') -> None:
__slots__ = ['start', 'end', 'text', 'type', 'signature', '_origin']

def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='', signature='') -> None:
warnings.warn("``Completion`` is a provisional API (as of IPython 6.0). "
"It may change without warnings. "
"Use in corresponding context manager.",
Expand All @@ -376,10 +381,12 @@ def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='
self.end = end
self.text = text
self.type = type
self.signature = signature
self._origin = _origin

def __repr__(self):
return '<Completion start=%s end=%s text=%r type=%r>' % (self.start, self.end, self.text, self.type or '?')
return '<Completion start=%s end=%s text=%r type=%r, signature=%r,>' % \
(self.start, self.end, self.text, self.type or '?', self.signature or '?')

def __eq__(self, other)->Bool:
"""
Expand Down Expand Up @@ -417,6 +424,10 @@ def _deduplicate_completions(text: str, completions: _IC)-> _IC:
completions: Iterator[Completion]
iterator over the completions to deduplicate
Yields
------
`Completions` objects
Completions coming from multiple sources, may be different but end up having
the same effect when applied to ``text``. If this is the case, this will
Expand Down Expand Up @@ -489,7 +500,7 @@ def rectify_completions(text: str, completions: _IC, *, _debug=False)->_IC:
seen_jedi.add(new_text)
elif c._origin == 'IPCompleter.python_matches':
seen_python_matches.add(new_text)
yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin)
yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin, signature=c.signature)
diff = seen_python_matches.difference(seen_jedi)
if diff and _debug:
print('IPython.python matches have extras:', diff)
Expand Down Expand Up @@ -933,6 +944,52 @@ def back_latex_name_matches(text:str):
return u'', ()


def _formatparamchildren(parameter) -> str:
"""
Get parameter name and value from Jedi Private API
Jedi does not expose a simple way to get `param=value` from its API.
Prameter
========
parameter:
Jedi's function `Param`
Returns
=======
A string like 'a', 'b=1', '*args', '**kwargs'
"""
description = parameter.description
if not description.startswith('param '):
raise ValueError('Jedi function parameter description have change format.'
'Expected "param ...", found %r".' % description)
return description[6:]

def _make_signature(completion)-> str:
"""
Make the signature from a jedi completion
Parameter
=========
completion: jedi.Completion
object does not complete a function type
Returns
=======
a string consisting of the function signature, with the parenthesis but
without the function name. example:
`(a, *args, b=1, **kwargs)`
"""

return '(%s)'% ', '.join([f for f in (_formatparamchildren(p) for p in completion.params) if f])

class IPCompleter(Completer):
"""Extension of the completer class with IPython-specific features"""

Expand Down Expand Up @@ -1762,10 +1819,15 @@ def _completions(self, full_text: str, offset: int, *, _timeout)->Iterator[Compl
print("Error in Jedi getting type of ", jm)
type_ = None
delta = len(jm.name_with_symbols) - len(jm.complete)
if type_ == 'function':
signature = _make_signature(jm)
else:
signature = ''
yield Completion(start=offset - delta,
end=offset,
text=jm.name_with_symbols,
type=type_,
signature=signature,
_origin='jedi')

if time.monotonic() > deadline:
Expand All @@ -1777,21 +1839,23 @@ def _completions(self, full_text: str, offset: int, *, _timeout)->Iterator[Compl
end=offset,
text=jm.name_with_symbols,
type='<unknown>', # don't compute type for speed
_origin='jedi')
_origin='jedi',
signature='')


start_offset = before.rfind(matched_text)

# TODO:
# Supress this, right now just for debug.
if jedi_matches and matches and self.debug:
yield Completion(start=start_offset, end=offset, text='--jedi/ipython--', _origin='debug')
yield Completion(start=start_offset, end=offset, text='--jedi/ipython--',
_origin='debug', type='none', signature='')

# I'm unsure if this is always true, so let's assert and see if it
# crash
assert before.endswith(matched_text)
for m, t in zip(matches, matches_origin):
yield Completion(start=start_offset, end=offset, text=m, _origin=t)
yield Completion(start=start_offset, end=offset, text=m, _origin=t, signature='', type='<unknown>')


def complete(self, text=None, line_buffer=None, cursor_pos=None):
Expand Down
6 changes: 3 additions & 3 deletions IPython/core/completerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ def try_import(mod: str, only_modules=False):
completions.extend(getattr(m, '__all__', []))
if m_is_init:
completions.extend(module_list(os.path.dirname(m.__file__)))
completions = {c for c in completions if isinstance(c, str)}
completions.discard('__init__')
return list(completions)
completions_set = {c for c in completions if isinstance(c, str)}
completions_set.discard('__init__')
return list(completions_set)


#-----------------------------------------------------------------------------
Expand Down
14 changes: 13 additions & 1 deletion IPython/core/tests/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ def _test_not_complete(reason, s, comp):

yield _test_not_complete, 'does not mix types', 'a=(1,"foo");a[0].', 'capitalize'

def test_completion_have_signature():
"""
Lets make sure jedi is capable of pulling out the signature of the function we are completing.
"""
ip = get_ipython()
with provisionalcompleter():
completions = ip.Completer.completions('ope', 3)
c = next(completions) # should be `open`
assert 'file' in c.signature, "Signature of function was not found by completer"
assert 'encoding' in c.signature, "Signature of function was not found by completer"


def test_deduplicate_completions():
"""
Test that completions are correctly deduplicated (even if ranges are not the same)
Expand Down Expand Up @@ -946,4 +958,4 @@ def test_snake_case_completion():
ip.user_ns['some_four'] = 4
_, matches = ip.complete("s_", "print(s_f")
nt.assert_in('some_three', matches)
nt.assert_in('some_four', matches)
nt.assert_in('some_four', matches)
6 changes: 3 additions & 3 deletions IPython/terminal/ptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ def _get_completions(body, offset, cursor_position, ipyc):

adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
if c.type == 'function':
display_text = display_text + '()'

yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()'), display_meta=c.type+c.signature)
else:
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)

class IPythonPTLexer(Lexer):
"""
Expand Down
2 changes: 2 additions & 0 deletions docs/source/interactive/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ IPython and Jedi will be able to infer that ``data[0]`` is actually a string
and should show relevant completions like ``upper()``, ``lower()`` and other
string methods. You can use the :kbd:`Tab` key to cycle through completions,
and while a completion is highlighted, its type will be shown as well.
When the type of the completion is a function, the completer will also show the
signature of the function when highlighted.

Exploring your objects
======================
Expand Down
4 changes: 4 additions & 0 deletions docs/source/whatsnew/pr/jedi-signature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Terminal IPython will now show the signature of the function while completing.
Only the currently highlighted function will show its signature on the line
below the completer by default. The functionality is recent so might be
limited, we welcome bug report and enhancement request on it.

0 comments on commit 6992964

Please sign in to comment.