Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e8c3f90

Browse files
committed
Show signature with Jedi.
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.
1 parent 57cf9b1 commit e8c3f90

6 files changed

Lines changed: 104 additions & 20 deletions

File tree

IPython/core/completer.py

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# encoding: utf-8
21
"""Completion for IPython.
32
43
This module started as fork of the rlcompleter module in the Python standard
@@ -87,7 +86,7 @@
8786
module in debug mode (start IPython with ``--Completer.debug=True``) in order
8887
to have extra logging information is :any:`jedi` is crashing, or if current
8988
IPython completer pending deprecations are returning results not yet handled
90-
by :any:`jedi`.
89+
by :any:`jedi`
9190
9291
Using Jedi for tab completion allow snippets like the following to work without
9392
having to execute any code:
@@ -103,8 +102,6 @@
103102
current development version to get better completions.
104103
"""
105104

106-
# skip module docstests
107-
skip_doctest = True
108105

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

142+
# skip module docstests
143+
skip_doctest = True
144+
145145
try:
146146
import jedi
147147
import jedi.api.helpers
148+
import jedi.api.classes
148149
JEDI_INSTALLED = True
149150
except ImportError:
150151
JEDI_INSTALLED = False
@@ -237,7 +238,7 @@ def protect_filename(s, protectables=PROTECTABLES):
237238
return s
238239

239240

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

279280

280-
def compress_user(path, tilde_expand, tilde_val):
281+
def compress_user(path:str, tilde_expand:bool, tilde_val:str) -> str:
281282
"""Does the opposite of expand_user, with its outputs.
282283
"""
283284
if tilde_expand:
@@ -338,6 +339,8 @@ def __init__(self, name):
338339
self.complete = name
339340
self.type = 'crashed'
340341
self.name_with_symbols = name
342+
self.signature = ''
343+
self._origin = 'fake'
341344

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

369-
def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='') -> None:
372+
__slots__ = ['start', 'end', 'text', 'type', 'signature', '_origin']
373+
374+
def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='', signature='') -> None:
370375
warnings.warn("``Completion`` is a provisional API (as of IPython 6.0). "
371376
"It may change without warnings. "
372377
"Use in corresponding context manager.",
@@ -376,10 +381,12 @@ def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='
376381
self.end = end
377382
self.text = text
378383
self.type = type
384+
self.signature = signature
379385
self._origin = _origin
380386

381387
def __repr__(self):
382-
return '<Completion start=%s end=%s text=%r type=%r>' % (self.start, self.end, self.text, self.type or '?')
388+
return '<Completion start=%s end=%s text=%r type=%r, signature=%r,>' % \
389+
(self.start, self.end, self.text, self.type or '?', self.signature or '?')
383390

384391
def __eq__(self, other)->Bool:
385392
"""
@@ -417,6 +424,10 @@ def _deduplicate_completions(text: str, completions: _IC)-> _IC:
417424
completions: Iterator[Completion]
418425
iterator over the completions to deduplicate
419426
427+
Yields
428+
------
429+
`Completions` objects
430+
420431
421432
Completions coming from multiple sources, may be different but end up having
422433
the same effect when applied to ``text``. If this is the case, this will
@@ -489,7 +500,7 @@ def rectify_completions(text: str, completions: _IC, *, _debug=False)->_IC:
489500
seen_jedi.add(new_text)
490501
elif c._origin == 'IPCompleter.python_matches':
491502
seen_python_matches.add(new_text)
492-
yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin)
503+
yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin, signature=c.signature)
493504
diff = seen_python_matches.difference(seen_jedi)
494505
if diff and _debug:
495506
print('IPython.python matches have extras:', diff)
@@ -933,6 +944,52 @@ def back_latex_name_matches(text:str):
933944
return u'', ()
934945

935946

947+
def _formatparamchildren(parameter) -> str:
948+
"""
949+
Get parameter name and value from Jedi Private API
950+
951+
Jedi does not expose a simple way to get `param=value` from its API.
952+
953+
Prameter
954+
========
955+
956+
parameter:
957+
Jedi's function `Param`
958+
959+
Returns
960+
=======
961+
962+
A string like 'a', 'b=1', '*args', '**kwargs'
963+
964+
965+
"""
966+
description = parameter.description
967+
if not description.startswith('param '):
968+
raise ValueError('Jedi function parameter description have change format.'
969+
'Expected "param ...", found %r".' % description)
970+
return description[6:]
971+
972+
def _make_signature(completion)-> str:
973+
"""
974+
Make the signature from a jedi completion
975+
976+
Parameter
977+
=========
978+
979+
completion: jedi.Completion
980+
object does not complete a function type
981+
982+
Returns
983+
=======
984+
985+
a string consisting of the function signature, with the parenthesis but
986+
without the function name. example:
987+
`(a, *args, b=1, **kwargs)`
988+
989+
"""
990+
991+
return '(%s)'% ', '.join([f for f in (_formatparamchildren(p) for p in completion.params) if f])
992+
936993
class IPCompleter(Completer):
937994
"""Extension of the completer class with IPython-specific features"""
938995

@@ -1762,10 +1819,15 @@ def _completions(self, full_text: str, offset: int, *, _timeout)->Iterator[Compl
17621819
print("Error in Jedi getting type of ", jm)
17631820
type_ = None
17641821
delta = len(jm.name_with_symbols) - len(jm.complete)
1822+
if type_ == 'function':
1823+
signature = _make_signature(jm)
1824+
else:
1825+
signature = ''
17651826
yield Completion(start=offset - delta,
17661827
end=offset,
17671828
text=jm.name_with_symbols,
17681829
type=type_,
1830+
signature=signature,
17691831
_origin='jedi')
17701832

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

17821845

17831846
start_offset = before.rfind(matched_text)
17841847

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

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

17961860

17971861
def complete(self, text=None, line_buffer=None, cursor_pos=None):

IPython/core/completerlib.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
# FIXME: this should be pulled in with the right call via the component system
3838
from IPython import get_ipython
3939

40+
from typing import List
41+
4042
#-----------------------------------------------------------------------------
4143
# Globals and constants
4244
#-----------------------------------------------------------------------------
@@ -153,7 +155,7 @@ def is_importable(module, attr, only_modules):
153155
return not(attr[:2] == '__' and attr[-2:] == '__')
154156

155157

156-
def try_import(mod: str, only_modules=False):
158+
def try_import(mod: str, only_modules=False) -> List[str]:
157159
"""
158160
Try to import given module and return list of potential completions.
159161
"""
@@ -173,9 +175,9 @@ def try_import(mod: str, only_modules=False):
173175
completions.extend(getattr(m, '__all__', []))
174176
if m_is_init:
175177
completions.extend(module_list(os.path.dirname(m.__file__)))
176-
completions = {c for c in completions if isinstance(c, str)}
177-
completions.discard('__init__')
178-
return list(completions)
178+
completions_set = {c for c in completions if isinstance(c, str)}
179+
completions_set.discard('__init__')
180+
return list(completions_set)
179181

180182

181183
#-----------------------------------------------------------------------------

IPython/core/tests/test_completer.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,18 @@ def _test_not_complete(reason, s, comp):
335335

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

338+
def test_completion_have_signature():
339+
"""
340+
Lets make sure jedi is capable of pulling out the signature of the function we are completing.
341+
"""
342+
ip = get_ipython()
343+
with provisionalcompleter():
344+
completions = ip.Completer.completions('ope', 3)
345+
c = next(completions) # should be `open`
346+
assert 'file' in c.signature, "Signature of function was not found by completer"
347+
assert 'encoding' in c.signature, "Signature of function was not found by completer"
348+
349+
338350
def test_deduplicate_completions():
339351
"""
340352
Test that completions are correctly deduplicated (even if ranges are not the same)
@@ -946,4 +958,4 @@ def test_snake_case_completion():
946958
ip.user_ns['some_four'] = 4
947959
_, matches = ip.complete("s_", "print(s_f")
948960
nt.assert_in('some_three', matches)
949-
nt.assert_in('some_four', matches)
961+
nt.assert_in('some_four', matches)

IPython/terminal/ptutils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ def _get_completions(body, offset, cursor_position, ipyc):
120120

121121
adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
122122
if c.type == 'function':
123-
display_text = display_text + '()'
124-
125-
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
123+
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()'), display_meta=c.type+c.signature)
124+
else:
125+
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
126126

127127
class IPythonPTLexer(Lexer):
128128
"""

docs/source/interactive/tutorial.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ IPython and Jedi will be able to infer that ``data[0]`` is actually a string
100100
and should show relevant completions like ``upper()``, ``lower()`` and other
101101
string methods. You can use the :kbd:`Tab` key to cycle through completions,
102102
and while a completion is highlighted, its type will be shown as well.
103+
When the type of the completion is a function, the completer will also show the
104+
signature of the function when highlighted.
103105

104106
Exploring your objects
105107
======================
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Terminal IPython will now show the signature of the function while completing.
2+
Only the currently highlighted function will show its signature on the line
3+
below the completer by default. The functionality is recent so might be
4+
limited, we welcome bug report and enhancement request on it.

0 commit comments

Comments
 (0)