diff --git a/IPython/core/completer.py b/IPython/core/completer.py index fbcf45a..029cfe1 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1,4 +1,3 @@ -# encoding: utf-8 """Completion for IPython. This module started as fork of the rlcompleter module in the Python standard @@ -87,7 +86,7 @@ We welcome any feedback on these new API, and we also encourage you to try this 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: @@ -103,8 +102,6 @@ Be sure to update :any:`jedi` to the latest stable version or to try the 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. @@ -142,9 +139,13 @@ from IPython.utils.dir2 import dir2, get_real_method 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 @@ -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 @@ -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: @@ -338,6 +339,8 @@ class _FakeJediCompletion: self.complete = name self.type = 'crashed' self.name_with_symbols = name + self.signature = '' + self._origin = 'fake' def __repr__(self): return '' @@ -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.", @@ -376,10 +381,12 @@ class Completion: self.end = end self.text = text self.type = type + self.signature = signature self._origin = _origin def __repr__(self): - return '' % (self.start, self.end, self.text, self.type or '?') + return '' % \ + (self.start, self.end, self.text, self.type or '?', self.signature or '?') def __eq__(self, other)->Bool: """ @@ -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 @@ -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) @@ -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""" @@ -1762,10 +1819,15 @@ class IPCompleter(Completer): 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: @@ -1777,7 +1839,8 @@ class IPCompleter(Completer): end=offset, text=jm.name_with_symbols, type='', # don't compute type for speed - _origin='jedi') + _origin='jedi', + signature='') start_offset = before.rfind(matched_text) @@ -1785,13 +1848,14 @@ class IPCompleter(Completer): # 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='') def complete(self, text=None, line_buffer=None, cursor_pos=None): diff --git a/IPython/core/completerlib.py b/IPython/core/completerlib.py index fbe086f..3c66d73 100644 --- a/IPython/core/completerlib.py +++ b/IPython/core/completerlib.py @@ -37,6 +37,8 @@ from IPython.utils._process_common import arg_split # FIXME: this should be pulled in with the right call via the component system from IPython import get_ipython +from typing import List + #----------------------------------------------------------------------------- # Globals and constants #----------------------------------------------------------------------------- @@ -153,7 +155,7 @@ def is_importable(module, attr, only_modules): return not(attr[:2] == '__' and attr[-2:] == '__') -def try_import(mod: str, only_modules=False): +def try_import(mod: str, only_modules=False) -> List[str]: """ Try to import given module and return list of potential completions. """ @@ -173,9 +175,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) #----------------------------------------------------------------------------- diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 1791588..d6e1f6c 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -335,6 +335,18 @@ def test_jedi(): 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) @@ -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) \ No newline at end of file + nt.assert_in('some_four', matches) diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py index 70140b4..94875c4 100644 --- a/IPython/terminal/ptutils.py +++ b/IPython/terminal/ptutils.py @@ -120,9 +120,9 @@ class IPythonPTCompleter(Completer): 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): """ diff --git a/docs/source/interactive/tutorial.rst b/docs/source/interactive/tutorial.rst index 2db814a..78bd76a 100644 --- a/docs/source/interactive/tutorial.rst +++ b/docs/source/interactive/tutorial.rst @@ -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 ====================== diff --git a/docs/source/whatsnew/pr/jedi-signature.rst b/docs/source/whatsnew/pr/jedi-signature.rst new file mode 100644 index 0000000..dc16497 --- /dev/null +++ b/docs/source/whatsnew/pr/jedi-signature.rst @@ -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.