##// END OF EJS Templates
fix IPCompleter inside tuples/arrays when jedi is disabled...
M Bussonnier -
Show More
@@ -184,6 +184,7 import glob
184 184 import inspect
185 185 import itertools
186 186 import keyword
187 import ast
187 188 import os
188 189 import re
189 190 import string
@@ -449,11 +450,11 def completions_sorting_key(word):
449 450
450 451 if word.startswith('%%'):
451 452 # 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:]:
453 454 word = word[2:]
454 455 prio2 = 2
455 456 elif word.startswith('%'):
456 if not "%" in word[1:]:
457 if "%" not in word[1:]:
457 458 word = word[1:]
458 459 prio2 = 1
459 460
@@ -961,8 +962,8 class CompletionSplitter(object):
961 962 def split_line(self, line, cursor_pos=None):
962 963 """Split a line of text with a cursor at the given position.
963 964 """
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]
966 967
967 968
968 969
@@ -1141,8 +1142,12 class Completer(Configurable):
1141 1142 """
1142 1143 return self._attr_matches(text)[0]
1143 1144
1145 # we simple attribute matching with normal identifiers.
1146 _ATTR_MATCH_RE = re.compile(r"(.+)\.(\w*)$")
1147
1144 1148 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)
1146 1151 if not m2:
1147 1152 return [], ""
1148 1153 expr, attr = m2.group(1, 2)
@@ -1204,6 +1209,30 class Completer(Configurable):
1204 1209 "." + attr,
1205 1210 )
1206 1211
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
1207 1236 def _evaluate_expr(self, expr):
1208 1237 obj = not_found
1209 1238 done = False
@@ -1225,14 +1254,14 class Completer(Configurable):
1225 1254 # e.g. user starts `(d[`, so we get `expr = '(d'`,
1226 1255 # where parenthesis is not closed.
1227 1256 # TODO: make this faster by reusing parts of the computation?
1228 expr = expr[1:]
1257 expr = self._trim_expr(expr)
1229 1258 return obj
1230 1259
1231 1260 def get__all__entries(obj):
1232 1261 """returns the strings in the __all__ attribute"""
1233 1262 try:
1234 1263 words = getattr(obj, '__all__')
1235 except:
1264 except Exception:
1236 1265 return []
1237 1266
1238 1267 return [w for w in words if isinstance(w, str)]
@@ -1447,7 +1476,7 def match_dict_keys(
1447 1476 try:
1448 1477 if not str_key.startswith(prefix_str):
1449 1478 continue
1450 except (AttributeError, TypeError, UnicodeError) as e:
1479 except (AttributeError, TypeError, UnicodeError):
1451 1480 # Python 3+ TypeError on b'a'.startswith('a') or vice-versa
1452 1481 continue
1453 1482
@@ -1495,7 +1524,7 def cursor_to_position(text:str, line:int, column:int)->int:
1495 1524 lines = text.split('\n')
1496 1525 assert line <= len(lines), '{} <= {}'.format(str(line), str(len(lines)))
1497 1526
1498 return sum(len(l) + 1 for l in lines[:line]) + column
1527 return sum(len(line) + 1 for line in lines[:line]) + column
1499 1528
1500 1529 def position_to_cursor(text:str, offset:int)->Tuple[int, int]:
1501 1530 """
@@ -2469,7 +2498,8 class IPCompleter(Completer):
2469 2498 # parenthesis before the cursor
2470 2499 # e.g. for "foo (1+bar(x), pa<cursor>,a=1)", the candidate is "foo"
2471 2500 tokens = regexp.findall(self.text_until_cursor)
2472 iterTokens = reversed(tokens); openPar = 0
2501 iterTokens = reversed(tokens)
2502 openPar = 0
2473 2503
2474 2504 for token in iterTokens:
2475 2505 if token == ')':
@@ -2489,7 +2519,8 class IPCompleter(Completer):
2489 2519 try:
2490 2520 ids.append(next(iterTokens))
2491 2521 if not isId(ids[-1]):
2492 ids.pop(); break
2522 ids.pop()
2523 break
2493 2524 if not next(iterTokens) == '.':
2494 2525 break
2495 2526 except StopIteration:
@@ -3215,7 +3246,7 class IPCompleter(Completer):
3215 3246 else:
3216 3247 api_version = _get_matcher_api_version(matcher)
3217 3248 raise ValueError(f"Unsupported API version {api_version}")
3218 except:
3249 except BaseException:
3219 3250 # Show the ugly traceback if the matcher causes an
3220 3251 # exception, but do NOT crash the kernel!
3221 3252 sys.excepthook(*sys.exc_info())
@@ -9,10 +9,10 import pytest
9 9 import sys
10 10 import textwrap
11 11 import unittest
12 import random
12 13
13 14 from importlib.metadata import version
14 15
15
16 16 from contextlib import contextmanager
17 17
18 18 from traitlets.config.loader import Config
@@ -21,6 +21,7 from IPython.core import completer
21 21 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
22 22 from IPython.utils.generics import complete_object
23 23 from IPython.testing import decorators as dec
24 from IPython.core.latex_symbols import latex_symbols
24 25
25 26 from IPython.core.completer import (
26 27 Completion,
@@ -31,11 +32,24 from IPython.core.completer import (
31 32 completion_matcher,
32 33 SimpleCompletion,
33 34 CompletionContext,
35 _unicode_name_compute,
36 _UNICODE_RANGES,
34 37 )
35 38
36 39 from packaging.version import parse
37 40
38 41
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
39 53 # -----------------------------------------------------------------------------
40 54 # Test functions
41 55 # -----------------------------------------------------------------------------
@@ -66,7 +80,7 def recompute_unicode_ranges():
66 80 rg = list(ranges(valid))
67 81 lens = []
68 82 gap_lens = []
69 pstart, pstop = 0, 0
83 _pstart, pstop = 0, 0
70 84 for start, stop in rg:
71 85 lens.append(stop - start)
72 86 gap_lens.append(
@@ -77,7 +91,7 def recompute_unicode_ranges():
77 91 f"{round((start - pstop)/0xe01f0*100)}%",
78 92 )
79 93 )
80 pstart, pstop = start, stop
94 _pstart, pstop = start, stop
81 95
82 96 return sorted(gap_lens)[-1]
83 97
@@ -87,7 +101,6 def test_unicode_range():
87 101 Test that the ranges we test for unicode names give the same number of
88 102 results than testing the full length.
89 103 """
90 from IPython.core.completer import _unicode_name_compute, _UNICODE_RANGES
91 104
92 105 expected_list = _unicode_name_compute([(0, 0x110000)])
93 106 test = _unicode_name_compute(_UNICODE_RANGES)
@@ -148,7 +161,6 def custom_matchers(matchers):
148 161 ip.Completer.custom_matchers.clear()
149 162
150 163
151 def test_protect_filename():
152 164 if sys.platform == "win32":
153 165 pairs = [
154 166 ("abc", "abc"),
@@ -183,10 +195,11 def test_protect_filename():
183 195 ("a^bc", r"a\^bc"),
184 196 ("a&bc", r"a\&bc"),
185 197 ]
186 # run the actual tests
187 for s1, s2 in pairs:
188 s1p = completer.protect_filename(s1)
189 assert s1p == s2
198
199
200 @pytest.mark.parametrize("s1,expected", pairs)
201 def test_protect_filename(s1, expected):
202 assert completer.protect_filename(s1) == expected
190 203
191 204
192 205 def check_line_split(splitter, test_specs):
@@ -297,8 +310,6 class TestCompleter(unittest.TestCase):
297 310 self.assertIsInstance(matches, list)
298 311
299 312 def test_latex_completions(self):
300 from IPython.core.latex_symbols import latex_symbols
301 import random
302 313
303 314 ip = get_ipython()
304 315 # Test some random unicode symbols
@@ -1735,6 +1746,45 class TestCompleter(unittest.TestCase):
1735 1746
1736 1747
1737 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
1787 @pytest.mark.parametrize(
1738 1788 "input, expected",
1739 1789 [
1740 1790 ["1.234", "1.234"],
General Comments 0
You need to be logged in to leave comments. Login now