From 9f622df42567de3b53e926ec2f26ce85293b0d86 2017-02-12 07:05:31 From: Fernando Perez Date: 2017-02-12 07:05:31 Subject: [PATCH] Merge pull request #10285 from Carreau/deduplicate-completions Deduplicate completions between IPython and Jedi. --- diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 969d23b..f60f919 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -344,11 +344,52 @@ class Completion: def __hash__(self): return hash((self.start, self.end, self.text)) + _IC = Iterator[Completion] -def rectify_completions(text:str, completion:_IC, *, _debug=False)->_IC: + +def _deduplicate_completions(text: str, completions: _IC)-> _IC: """ - Rectify a set of completion to all have the same ``start`` and ``end`` + Deduplicate a set of completions. + + .. warning:: Unstable + + This function is unstable, API may change without warning. + + Parameters + ---------- + text: str + text that should be completed. + completions: Iterator[Completion] + iterator over the completions to deduplicate + + + 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 + consider completions as equal and only emit the first encountered. + + Not folded in `completions()` yet for debugging purpose, and to detect when + the IPython completer does return things that Jedi does not, but should be + at some point. + """ + completions = list(completions) + if not completions: + return + + new_start = min(c.start for c in completions) + new_end = max(c.end for c in completions) + + seen = set() + for c in completions: + new_text = text[new_start:c.start] + c.text + text[c.end:new_end] + if new_text not in seen: + yield c + seen.add(new_text) + + +def rectify_completions(text: str, completions: _IC, *, _debug=False)->_IC: + """ + Rectify a set of completions to all have the same ``start`` and ``end`` .. warning:: Unstable @@ -359,7 +400,7 @@ def rectify_completions(text:str, completion:_IC, *, _debug=False)->_IC: ---------- text: str text that should be completed. - completion: Iterator[Completion] + completions: Iterator[Completion] iterator over the completions to rectify @@ -1510,22 +1551,17 @@ class IPCompleter(Completer): fake Completion token to distinguish completion returned by Jedi and usual IPython completion. + .. note:: + + Completions are not completely deduplicated yet. If identical + completions are coming from different sources this function does not + ensure that each completion object will only be present once. """ warnings.warn("_complete is a provisional API (as of IPython 6.0). " "It may change without warnings. " "Use in corresponding context manager.", category=ProvisionalCompleterWarning, stacklevel=2) - # Possible Improvements / Known limitation - ########################################## - # Completions may be identical even if they have different ranges and - # text. For example: - # >>> a=1 - # >>> a. - # May returns: - # - `a.real` from 0 to 2 - # - `.real` from 1 to 2 - # the current code does not (yet) check for such equivalence seen = set() for c in self._completions(text, offset, _timeout=self.jedi_compute_type_timeout/1000): if c and (c in seen): diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index cd193b9..533ba42 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -6,6 +6,7 @@ import os import sys +import textwrap import unittest from contextlib import contextmanager @@ -20,7 +21,8 @@ from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory from IPython.utils.generics import complete_object from IPython.testing import decorators as dec -from IPython.core.completer import Completion, provisionalcompleter, match_dict_keys +from IPython.core.completer import ( + Completion, provisionalcompleter, match_dict_keys, _deduplicate_completions) from nose.tools import assert_in, assert_not_in #----------------------------------------------------------------------------- @@ -294,7 +296,7 @@ def test_jedi(): import jedi jedi_version = tuple(int(i) for i in jedi.__version__.split('.')[:3]) - if jedi_version > (0,10): + if jedi_version > (0, 10): yield _test_complete, 'jedi >0.9 should complete and not crash', 'a=1;a.', 'real' yield _test_complete, 'can infer first argument', 'a=(1,"foo");a[0].', 'real' yield _test_complete, 'can infer second argument', 'a=(1,"foo");a[1].', 'capitalize' @@ -302,6 +304,21 @@ def test_jedi(): yield _test_not_complete, 'does not mix types', 'a=(1,"foo");a[0].', 'capitalize' +def test_deduplicate_completions(): + """ + Test that completions are correctly deduplicated (even if ranges are not the same) + """ + ip = get_ipython() + ip.ex(textwrap.dedent(''' + class Z: + zoo = 1 + ''')) + with provisionalcompleter(): + l = list(_deduplicate_completions('Z.z', ip.Completer.completions('Z.z', 3))) + + assert len(l) == 1, 'Completions (Z.z) correctly deduplicate: %s ' % l + assert l[0].text == 'zoo' # and not `it.accumulate` + def test_greedy_completions(): """ diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py index d46f017..a1d258d 100644 --- a/IPython/terminal/ptutils.py +++ b/IPython/terminal/ptutils.py @@ -10,7 +10,9 @@ not to be used outside IPython. import unicodedata from wcwidth import wcwidth -from IPython.core.completer import IPCompleter, provisionalcompleter, rectify_completions, cursor_to_position +from IPython.core.completer import ( + IPCompleter, provisionalcompleter, rectify_completions, cursor_to_position, + _deduplicate_completions) from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.layout.lexers import Lexer from prompt_toolkit.layout.lexers import PygmentsLexer @@ -61,8 +63,8 @@ class IPythonPTCompleter(Completer): Private equivalent of get_completions() use only for unit_testing. """ debug = getattr(ipyc, 'debug', False) - completions = rectify_completions( - body, ipyc.completions(body, offset), _debug=debug) + completions = _deduplicate_completions( + body, ipyc.completions(body, offset)) for c in completions: if not c.text: # Guard against completion machinery giving us an empty string.