Skip to content

Commit ce148f2

Browse files
committed
fix IPCompleter inside tuples/arrays when jedi is disabled
The selection of the current expression was improperly finding an implicit tuple `a,b`, instead of trimming tosimply `b`. I also done a number of simplification of test cases.
1 parent be84e4b commit ce148f2

File tree

2 files changed

+138
-57
lines changed

2 files changed

+138
-57
lines changed

IPython/core/completer.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
import inspect
185185
import itertools
186186
import keyword
187+
import ast
187188
import os
188189
import re
189190
import string
@@ -449,11 +450,11 @@ def completions_sorting_key(word):
449450

450451
if word.startswith('%%'):
451452
# If there's another % in there, this is something else, so leave it alone
452-
if not "%" in word[2:]:
453+
if "%" not in word[2:]:
453454
word = word[2:]
454455
prio2 = 2
455456
elif word.startswith('%'):
456-
if not "%" in word[1:]:
457+
if "%" not in word[1:]:
457458
word = word[1:]
458459
prio2 = 1
459460

@@ -961,8 +962,8 @@ def delims(self, delims):
961962
def split_line(self, line, cursor_pos=None):
962963
"""Split a line of text with a cursor at the given position.
963964
"""
964-
l = line if cursor_pos is None else line[:cursor_pos]
965-
return self._delim_re.split(l)[-1]
965+
cut_line = line if cursor_pos is None else line[:cursor_pos]
966+
return self._delim_re.split(cut_line)[-1]
966967

967968

968969

@@ -1141,8 +1142,12 @@ def attr_matches(self, text):
11411142
"""
11421143
return self._attr_matches(text)[0]
11431144

1145+
# we simple attribute matching with normal identifiers.
1146+
_ATTR_MATCH_RE = re.compile(r"(.+)\.(\w*)$")
1147+
11441148
def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]:
1145-
m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer)
1149+
1150+
m2 = self._ATTR_MATCH_RE.match(self.line_buffer)
11461151
if not m2:
11471152
return [], ""
11481153
expr, attr = m2.group(1, 2)
@@ -1204,6 +1209,30 @@ def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]:
12041209
"." + attr,
12051210
)
12061211

1212+
def _trim_expr(self, code: str) -> str:
1213+
"""
1214+
Trim the code until it is a valid expression and not a tuple;
1215+
1216+
return the trimmed expression for guarded_eval.
1217+
"""
1218+
while code:
1219+
code = code[1:]
1220+
try:
1221+
res = ast.parse(code)
1222+
except SyntaxError:
1223+
continue
1224+
1225+
assert res is not None
1226+
if len(res.body) != 1:
1227+
continue
1228+
expr = res.body[0].value
1229+
if isinstance(expr, ast.Tuple) and not code[-1] == ")":
1230+
# we skip implicit tuple, like when trimming `fun(a,b`<completion>
1231+
# as `a,b` would be a tuple, and we actually expect to get only `b`
1232+
continue
1233+
return code
1234+
return ""
1235+
12071236
def _evaluate_expr(self, expr):
12081237
obj = not_found
12091238
done = False
@@ -1225,14 +1254,14 @@ def _evaluate_expr(self, expr):
12251254
# e.g. user starts `(d[`, so we get `expr = '(d'`,
12261255
# where parenthesis is not closed.
12271256
# TODO: make this faster by reusing parts of the computation?
1228-
expr = expr[1:]
1257+
expr = self._trim_expr(expr)
12291258
return obj
12301259

12311260
def get__all__entries(obj):
12321261
"""returns the strings in the __all__ attribute"""
12331262
try:
12341263
words = getattr(obj, '__all__')
1235-
except:
1264+
except Exception:
12361265
return []
12371266

12381267
return [w for w in words if isinstance(w, str)]
@@ -1447,7 +1476,7 @@ def filter_prefix_tuple(key):
14471476
try:
14481477
if not str_key.startswith(prefix_str):
14491478
continue
1450-
except (AttributeError, TypeError, UnicodeError) as e:
1479+
except (AttributeError, TypeError, UnicodeError):
14511480
# Python 3+ TypeError on b'a'.startswith('a') or vice-versa
14521481
continue
14531482

@@ -1495,7 +1524,7 @@ def cursor_to_position(text:str, line:int, column:int)->int:
14951524
lines = text.split('\n')
14961525
assert line <= len(lines), '{} <= {}'.format(str(line), str(len(lines)))
14971526

1498-
return sum(len(l) + 1 for l in lines[:line]) + column
1527+
return sum(len(line) + 1 for line in lines[:line]) + column
14991528

15001529
def position_to_cursor(text:str, offset:int)->Tuple[int, int]:
15011530
"""
@@ -2469,7 +2498,8 @@ def python_func_kw_matches(self, text):
24692498
# parenthesis before the cursor
24702499
# e.g. for "foo (1+bar(x), pa<cursor>,a=1)", the candidate is "foo"
24712500
tokens = regexp.findall(self.text_until_cursor)
2472-
iterTokens = reversed(tokens); openPar = 0
2501+
iterTokens = reversed(tokens)
2502+
openPar = 0
24732503

24742504
for token in iterTokens:
24752505
if token == ')':
@@ -2489,7 +2519,8 @@ def python_func_kw_matches(self, text):
24892519
try:
24902520
ids.append(next(iterTokens))
24912521
if not isId(ids[-1]):
2492-
ids.pop(); break
2522+
ids.pop()
2523+
break
24932524
if not next(iterTokens) == '.':
24942525
break
24952526
except StopIteration:
@@ -3215,7 +3246,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
32153246
else:
32163247
api_version = _get_matcher_api_version(matcher)
32173248
raise ValueError(f"Unsupported API version {api_version}")
3218-
except:
3249+
except BaseException:
32193250
# Show the ugly traceback if the matcher causes an
32203251
# exception, but do NOT crash the kernel!
32213252
sys.excepthook(*sys.exc_info())

IPython/core/tests/test_completer.py

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import sys
1010
import textwrap
1111
import unittest
12+
import random
1213

1314
from importlib.metadata import version
1415

15-
1616
from contextlib import contextmanager
1717

1818
from traitlets.config.loader import Config
@@ -21,6 +21,7 @@
2121
from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
2222
from IPython.utils.generics import complete_object
2323
from IPython.testing import decorators as dec
24+
from IPython.core.latex_symbols import latex_symbols
2425

2526
from IPython.core.completer import (
2627
Completion,
@@ -31,11 +32,24 @@
3132
completion_matcher,
3233
SimpleCompletion,
3334
CompletionContext,
35+
_unicode_name_compute,
36+
_UNICODE_RANGES,
3437
)
3538

3639
from packaging.version import parse
3740

3841

42+
@contextmanager
43+
def jedi_status(status: bool):
44+
completer = get_ipython().Completer
45+
try:
46+
old = completer.use_jedi
47+
completer.use_jedi = status
48+
yield
49+
finally:
50+
completer.use_jedi = old
51+
52+
3953
# -----------------------------------------------------------------------------
4054
# Test functions
4155
# -----------------------------------------------------------------------------
@@ -66,7 +80,7 @@ def ranges(i):
6680
rg = list(ranges(valid))
6781
lens = []
6882
gap_lens = []
69-
pstart, pstop = 0, 0
83+
_pstart, pstop = 0, 0
7084
for start, stop in rg:
7185
lens.append(stop - start)
7286
gap_lens.append(
@@ -77,7 +91,7 @@ def ranges(i):
7791
f"{round((start - pstop)/0xe01f0*100)}%",
7892
)
7993
)
80-
pstart, pstop = start, stop
94+
_pstart, pstop = start, stop
8195

8296
return sorted(gap_lens)[-1]
8397

@@ -87,7 +101,6 @@ def test_unicode_range():
87101
Test that the ranges we test for unicode names give the same number of
88102
results than testing the full length.
89103
"""
90-
from IPython.core.completer import _unicode_name_compute, _UNICODE_RANGES
91104

92105
expected_list = _unicode_name_compute([(0, 0x110000)])
93106
test = _unicode_name_compute(_UNICODE_RANGES)
@@ -148,45 +161,45 @@ def custom_matchers(matchers):
148161
ip.Completer.custom_matchers.clear()
149162

150163

151-
def test_protect_filename():
152-
if sys.platform == "win32":
153-
pairs = [
154-
("abc", "abc"),
155-
(" abc", '" abc"'),
156-
("a bc", '"a bc"'),
157-
("a bc", '"a bc"'),
158-
(" bc", '" bc"'),
159-
]
160-
else:
161-
pairs = [
162-
("abc", "abc"),
163-
(" abc", r"\ abc"),
164-
("a bc", r"a\ bc"),
165-
("a bc", r"a\ \ bc"),
166-
(" bc", r"\ \ bc"),
167-
# On posix, we also protect parens and other special characters.
168-
("a(bc", r"a\(bc"),
169-
("a)bc", r"a\)bc"),
170-
("a( )bc", r"a\(\ \)bc"),
171-
("a[1]bc", r"a\[1\]bc"),
172-
("a{1}bc", r"a\{1\}bc"),
173-
("a#bc", r"a\#bc"),
174-
("a?bc", r"a\?bc"),
175-
("a=bc", r"a\=bc"),
176-
("a\\bc", r"a\\bc"),
177-
("a|bc", r"a\|bc"),
178-
("a;bc", r"a\;bc"),
179-
("a:bc", r"a\:bc"),
180-
("a'bc", r"a\'bc"),
181-
("a*bc", r"a\*bc"),
182-
('a"bc', r"a\"bc"),
183-
("a^bc", r"a\^bc"),
184-
("a&bc", r"a\&bc"),
185-
]
186-
# run the actual tests
187-
for s1, s2 in pairs:
188-
s1p = completer.protect_filename(s1)
189-
assert s1p == s2
164+
if sys.platform == "win32":
165+
pairs = [
166+
("abc", "abc"),
167+
(" abc", '" abc"'),
168+
("a bc", '"a bc"'),
169+
("a bc", '"a bc"'),
170+
(" bc", '" bc"'),
171+
]
172+
else:
173+
pairs = [
174+
("abc", "abc"),
175+
(" abc", r"\ abc"),
176+
("a bc", r"a\ bc"),
177+
("a bc", r"a\ \ bc"),
178+
(" bc", r"\ \ bc"),
179+
# On posix, we also protect parens and other special characters.
180+
("a(bc", r"a\(bc"),
181+
("a)bc", r"a\)bc"),
182+
("a( )bc", r"a\(\ \)bc"),
183+
("a[1]bc", r"a\[1\]bc"),
184+
("a{1}bc", r"a\{1\}bc"),
185+
("a#bc", r"a\#bc"),
186+
("a?bc", r"a\?bc"),
187+
("a=bc", r"a\=bc"),
188+
("a\\bc", r"a\\bc"),
189+
("a|bc", r"a\|bc"),
190+
("a;bc", r"a\;bc"),
191+
("a:bc", r"a\:bc"),
192+
("a'bc", r"a\'bc"),
193+
("a*bc", r"a\*bc"),
194+
('a"bc', r"a\"bc"),
195+
("a^bc", r"a\^bc"),
196+
("a&bc", r"a\&bc"),
197+
]
198+
199+
200+
@pytest.mark.parametrize("s1,expected", pairs)
201+
def test_protect_filename(s1, expected):
202+
assert completer.protect_filename(s1) == expected
190203

191204

192205
def check_line_split(splitter, test_specs):
@@ -297,8 +310,6 @@ def test_unicode_completions(self):
297310
self.assertIsInstance(matches, list)
298311

299312
def test_latex_completions(self):
300-
from IPython.core.latex_symbols import latex_symbols
301-
import random
302313

303314
ip = get_ipython()
304315
# Test some random unicode symbols
@@ -1734,6 +1745,45 @@ def _(expected):
17341745
_(["completion_a"])
17351746

17361747

1748+
@pytest.mark.parametrize(
1749+
"setup,code,expected,not_expected",
1750+
[
1751+
('a="str"; b=1', "(a, b.", [".bit_count", ".conjugate"], [".count"]),
1752+
('a="str"; b=1', "(a, b).", [".count"], [".bit_count", ".capitalize"]),
1753+
('x="str"; y=1', "x = {1, y.", [".bit_count"], [".count"]),
1754+
('x="str"; y=1', "x = [1, y.", [".bit_count"], [".count"]),
1755+
('x="str"; y=1; fun=lambda x:x', "x = fun(1, y.", [".bit_count"], [".count"]),
1756+
],
1757+
)
1758+
def test_misc_no_jedi_completions(setup, code, expected, not_expected):
1759+
ip = get_ipython()
1760+
c = ip.Completer
1761+
ip.ex(setup)
1762+
with provisionalcompleter(), jedi_status(False):
1763+
matches = c.all_completions(code)
1764+
assert set(expected) - set(matches) == set(), set(matches)
1765+
assert set(matches).intersection(set(not_expected)) == set()
1766+
1767+
1768+
@pytest.mark.parametrize(
1769+
"code,expected",
1770+
[
1771+
(" (a, b", "b"),
1772+
("(a, b", "b"),
1773+
("(a, b)", ""), # trim always start by trimming
1774+
(" (a, b)", "(a, b)"),
1775+
(" [a, b]", "[a, b]"),
1776+
(" a, b", "b"),
1777+
("x = {1, y", "y"),
1778+
("x = [1, y", "y"),
1779+
("x = fun(1, y", "y"),
1780+
],
1781+
)
1782+
def test_trim_expr(code, expected):
1783+
c = get_ipython().Completer
1784+
assert c._trim_expr(code) == expected
1785+
1786+
17371787
@pytest.mark.parametrize(
17381788
"input, expected",
17391789
[

0 commit comments

Comments
 (0)