##// END OF EJS Templates
Fix completion tuple (#14594)...
M Bussonnier -
r28978:9cdf92d3 merge
parent child Browse files
Show More
@@ -184,6 +184,7 import glob
184 import inspect
184 import inspect
185 import itertools
185 import itertools
186 import keyword
186 import keyword
187 import ast
187 import os
188 import os
188 import re
189 import re
189 import string
190 import string
@@ -347,7 +348,7 def provisionalcompleter(action='ignore'):
347 yield
348 yield
348
349
349
350
350 def has_open_quotes(s):
351 def has_open_quotes(s: str) -> Union[str, bool]:
351 """Return whether a string has open quotes.
352 """Return whether a string has open quotes.
352
353
353 This simply counts whether the number of quote characters of either type in
354 This simply counts whether the number of quote characters of either type in
@@ -368,7 +369,7 def has_open_quotes(s):
368 return False
369 return False
369
370
370
371
371 def protect_filename(s, protectables=PROTECTABLES):
372 def protect_filename(s: str, protectables: str = PROTECTABLES) -> str:
372 """Escape a string to protect certain characters."""
373 """Escape a string to protect certain characters."""
373 if set(s) & set(protectables):
374 if set(s) & set(protectables):
374 if sys.platform == "win32":
375 if sys.platform == "win32":
@@ -449,11 +450,11 def completions_sorting_key(word):
449
450
450 if word.startswith('%%'):
451 if word.startswith('%%'):
451 # If there's another % in there, this is something else, so leave it alone
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 word = word[2:]
454 word = word[2:]
454 prio2 = 2
455 prio2 = 2
455 elif word.startswith('%'):
456 elif word.startswith('%'):
456 if not "%" in word[1:]:
457 if "%" not in word[1:]:
457 word = word[1:]
458 word = word[1:]
458 prio2 = 1
459 prio2 = 1
459
460
@@ -752,7 +753,7 def completion_matcher(
752 priority: Optional[float] = None,
753 priority: Optional[float] = None,
753 identifier: Optional[str] = None,
754 identifier: Optional[str] = None,
754 api_version: int = 1,
755 api_version: int = 1,
755 ):
756 ) -> Callable[[Matcher], Matcher]:
756 """Adds attributes describing the matcher.
757 """Adds attributes describing the matcher.
757
758
758 Parameters
759 Parameters
@@ -961,8 +962,8 class CompletionSplitter(object):
961 def split_line(self, line, cursor_pos=None):
962 def split_line(self, line, cursor_pos=None):
962 """Split a line of text with a cursor at the given position.
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 cut_line = line if cursor_pos is None else line[:cursor_pos]
965 return self._delim_re.split(l)[-1]
966 return self._delim_re.split(cut_line)[-1]
966
967
967
968
968
969
@@ -1141,8 +1142,13 class Completer(Configurable):
1141 """
1142 """
1142 return self._attr_matches(text)[0]
1143 return self._attr_matches(text)[0]
1143
1144
1144 def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]:
1145 # we simple attribute matching with normal identifiers.
1145 m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer)
1146 _ATTR_MATCH_RE = re.compile(r"(.+)\.(\w*)$")
1147
1148 def _attr_matches(
1149 self, text: str, include_prefix: bool = True
1150 ) -> Tuple[Sequence[str], str]:
1151 m2 = self._ATTR_MATCH_RE.match(self.line_buffer)
1146 if not m2:
1152 if not m2:
1147 return [], ""
1153 return [], ""
1148 expr, attr = m2.group(1, 2)
1154 expr, attr = m2.group(1, 2)
@@ -1204,6 +1210,30 class Completer(Configurable):
1204 "." + attr,
1210 "." + attr,
1205 )
1211 )
1206
1212
1213 def _trim_expr(self, code: str) -> str:
1214 """
1215 Trim the code until it is a valid expression and not a tuple;
1216
1217 return the trimmed expression for guarded_eval.
1218 """
1219 while code:
1220 code = code[1:]
1221 try:
1222 res = ast.parse(code)
1223 except SyntaxError:
1224 continue
1225
1226 assert res is not None
1227 if len(res.body) != 1:
1228 continue
1229 expr = res.body[0].value
1230 if isinstance(expr, ast.Tuple) and not code[-1] == ")":
1231 # we skip implicit tuple, like when trimming `fun(a,b`<completion>
1232 # as `a,b` would be a tuple, and we actually expect to get only `b`
1233 continue
1234 return code
1235 return ""
1236
1207 def _evaluate_expr(self, expr):
1237 def _evaluate_expr(self, expr):
1208 obj = not_found
1238 obj = not_found
1209 done = False
1239 done = False
@@ -1225,14 +1255,14 class Completer(Configurable):
1225 # e.g. user starts `(d[`, so we get `expr = '(d'`,
1255 # e.g. user starts `(d[`, so we get `expr = '(d'`,
1226 # where parenthesis is not closed.
1256 # where parenthesis is not closed.
1227 # TODO: make this faster by reusing parts of the computation?
1257 # TODO: make this faster by reusing parts of the computation?
1228 expr = expr[1:]
1258 expr = self._trim_expr(expr)
1229 return obj
1259 return obj
1230
1260
1231 def get__all__entries(obj):
1261 def get__all__entries(obj):
1232 """returns the strings in the __all__ attribute"""
1262 """returns the strings in the __all__ attribute"""
1233 try:
1263 try:
1234 words = getattr(obj, '__all__')
1264 words = getattr(obj, '__all__')
1235 except:
1265 except Exception:
1236 return []
1266 return []
1237
1267
1238 return [w for w in words if isinstance(w, str)]
1268 return [w for w in words if isinstance(w, str)]
@@ -1447,7 +1477,7 def match_dict_keys(
1447 try:
1477 try:
1448 if not str_key.startswith(prefix_str):
1478 if not str_key.startswith(prefix_str):
1449 continue
1479 continue
1450 except (AttributeError, TypeError, UnicodeError) as e:
1480 except (AttributeError, TypeError, UnicodeError):
1451 # Python 3+ TypeError on b'a'.startswith('a') or vice-versa
1481 # Python 3+ TypeError on b'a'.startswith('a') or vice-versa
1452 continue
1482 continue
1453
1483
@@ -1495,7 +1525,7 def cursor_to_position(text:str, line:int, column:int)->int:
1495 lines = text.split('\n')
1525 lines = text.split('\n')
1496 assert line <= len(lines), '{} <= {}'.format(str(line), str(len(lines)))
1526 assert line <= len(lines), '{} <= {}'.format(str(line), str(len(lines)))
1497
1527
1498 return sum(len(l) + 1 for l in lines[:line]) + column
1528 return sum(len(line) + 1 for line in lines[:line]) + column
1499
1529
1500 def position_to_cursor(text:str, offset:int)->Tuple[int, int]:
1530 def position_to_cursor(text:str, offset:int)->Tuple[int, int]:
1501 """
1531 """
@@ -2112,7 +2142,7 class IPCompleter(Completer):
2112 result["suppress"] = is_magic_prefix and bool(result["completions"])
2142 result["suppress"] = is_magic_prefix and bool(result["completions"])
2113 return result
2143 return result
2114
2144
2115 def magic_matches(self, text: str):
2145 def magic_matches(self, text: str) -> List[str]:
2116 """Match magics.
2146 """Match magics.
2117
2147
2118 .. deprecated:: 8.6
2148 .. deprecated:: 8.6
@@ -2469,7 +2499,8 class IPCompleter(Completer):
2469 # parenthesis before the cursor
2499 # parenthesis before the cursor
2470 # e.g. for "foo (1+bar(x), pa<cursor>,a=1)", the candidate is "foo"
2500 # e.g. for "foo (1+bar(x), pa<cursor>,a=1)", the candidate is "foo"
2471 tokens = regexp.findall(self.text_until_cursor)
2501 tokens = regexp.findall(self.text_until_cursor)
2472 iterTokens = reversed(tokens); openPar = 0
2502 iterTokens = reversed(tokens)
2503 openPar = 0
2473
2504
2474 for token in iterTokens:
2505 for token in iterTokens:
2475 if token == ')':
2506 if token == ')':
@@ -2489,7 +2520,8 class IPCompleter(Completer):
2489 try:
2520 try:
2490 ids.append(next(iterTokens))
2521 ids.append(next(iterTokens))
2491 if not isId(ids[-1]):
2522 if not isId(ids[-1]):
2492 ids.pop(); break
2523 ids.pop()
2524 break
2493 if not next(iterTokens) == '.':
2525 if not next(iterTokens) == '.':
2494 break
2526 break
2495 except StopIteration:
2527 except StopIteration:
@@ -3215,7 +3247,7 class IPCompleter(Completer):
3215 else:
3247 else:
3216 api_version = _get_matcher_api_version(matcher)
3248 api_version = _get_matcher_api_version(matcher)
3217 raise ValueError(f"Unsupported API version {api_version}")
3249 raise ValueError(f"Unsupported API version {api_version}")
3218 except:
3250 except BaseException:
3219 # Show the ugly traceback if the matcher causes an
3251 # Show the ugly traceback if the matcher causes an
3220 # exception, but do NOT crash the kernel!
3252 # exception, but do NOT crash the kernel!
3221 sys.excepthook(*sys.exc_info())
3253 sys.excepthook(*sys.exc_info())
@@ -9,10 +9,10 import pytest
9 import sys
9 import sys
10 import textwrap
10 import textwrap
11 import unittest
11 import unittest
12 import random
12
13
13 from importlib.metadata import version
14 from importlib.metadata import version
14
15
15
16 from contextlib import contextmanager
16 from contextlib import contextmanager
17
17
18 from traitlets.config.loader import Config
18 from traitlets.config.loader import Config
@@ -21,6 +21,7 from IPython.core import completer
21 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
21 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
22 from IPython.utils.generics import complete_object
22 from IPython.utils.generics import complete_object
23 from IPython.testing import decorators as dec
23 from IPython.testing import decorators as dec
24 from IPython.core.latex_symbols import latex_symbols
24
25
25 from IPython.core.completer import (
26 from IPython.core.completer import (
26 Completion,
27 Completion,
@@ -31,11 +32,24 from IPython.core.completer import (
31 completion_matcher,
32 completion_matcher,
32 SimpleCompletion,
33 SimpleCompletion,
33 CompletionContext,
34 CompletionContext,
35 _unicode_name_compute,
36 _UNICODE_RANGES,
34 )
37 )
35
38
36 from packaging.version import parse
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 # Test functions
54 # Test functions
41 # -----------------------------------------------------------------------------
55 # -----------------------------------------------------------------------------
@@ -66,7 +80,7 def recompute_unicode_ranges():
66 rg = list(ranges(valid))
80 rg = list(ranges(valid))
67 lens = []
81 lens = []
68 gap_lens = []
82 gap_lens = []
69 pstart, pstop = 0, 0
83 _pstart, pstop = 0, 0
70 for start, stop in rg:
84 for start, stop in rg:
71 lens.append(stop - start)
85 lens.append(stop - start)
72 gap_lens.append(
86 gap_lens.append(
@@ -77,7 +91,7 def recompute_unicode_ranges():
77 f"{round((start - pstop)/0xe01f0*100)}%",
91 f"{round((start - pstop)/0xe01f0*100)}%",
78 )
92 )
79 )
93 )
80 pstart, pstop = start, stop
94 _pstart, pstop = start, stop
81
95
82 return sorted(gap_lens)[-1]
96 return sorted(gap_lens)[-1]
83
97
@@ -87,7 +101,6 def test_unicode_range():
87 Test that the ranges we test for unicode names give the same number of
101 Test that the ranges we test for unicode names give the same number of
88 results than testing the full length.
102 results than testing the full length.
89 """
103 """
90 from IPython.core.completer import _unicode_name_compute, _UNICODE_RANGES
91
104
92 expected_list = _unicode_name_compute([(0, 0x110000)])
105 expected_list = _unicode_name_compute([(0, 0x110000)])
93 test = _unicode_name_compute(_UNICODE_RANGES)
106 test = _unicode_name_compute(_UNICODE_RANGES)
@@ -148,45 +161,45 def custom_matchers(matchers):
148 ip.Completer.custom_matchers.clear()
161 ip.Completer.custom_matchers.clear()
149
162
150
163
151 def test_protect_filename():
164 if sys.platform == "win32":
152 if sys.platform == "win32":
165 pairs = [
153 pairs = [
166 ("abc", "abc"),
154 ("abc", "abc"),
167 (" abc", '" abc"'),
155 (" abc", '" abc"'),
168 ("a bc", '"a bc"'),
156 ("a bc", '"a bc"'),
169 ("a bc", '"a bc"'),
157 ("a bc", '"a bc"'),
170 (" bc", '" bc"'),
158 (" bc", '" bc"'),
171 ]
159 ]
172 else:
160 else:
173 pairs = [
161 pairs = [
174 ("abc", "abc"),
162 ("abc", "abc"),
175 (" abc", r"\ abc"),
163 (" abc", r"\ abc"),
176 ("a bc", r"a\ bc"),
164 ("a bc", r"a\ bc"),
177 ("a bc", r"a\ \ bc"),
165 ("a bc", r"a\ \ bc"),
178 (" bc", r"\ \ bc"),
166 (" bc", r"\ \ bc"),
179 # On posix, we also protect parens and other special characters.
167 # On posix, we also protect parens and other special characters.
180 ("a(bc", r"a\(bc"),
168 ("a(bc", r"a\(bc"),
181 ("a)bc", r"a\)bc"),
169 ("a)bc", r"a\)bc"),
182 ("a( )bc", r"a\(\ \)bc"),
170 ("a( )bc", r"a\(\ \)bc"),
183 ("a[1]bc", r"a\[1\]bc"),
171 ("a[1]bc", r"a\[1\]bc"),
184 ("a{1}bc", r"a\{1\}bc"),
172 ("a{1}bc", r"a\{1\}bc"),
185 ("a#bc", r"a\#bc"),
173 ("a#bc", r"a\#bc"),
186 ("a?bc", r"a\?bc"),
174 ("a?bc", r"a\?bc"),
187 ("a=bc", r"a\=bc"),
175 ("a=bc", r"a\=bc"),
188 ("a\\bc", r"a\\bc"),
176 ("a\\bc", r"a\\bc"),
189 ("a|bc", r"a\|bc"),
177 ("a|bc", r"a\|bc"),
190 ("a;bc", r"a\;bc"),
178 ("a;bc", r"a\;bc"),
191 ("a:bc", r"a\:bc"),
179 ("a:bc", r"a\:bc"),
192 ("a'bc", r"a\'bc"),
180 ("a'bc", r"a\'bc"),
193 ("a*bc", r"a\*bc"),
181 ("a*bc", r"a\*bc"),
194 ('a"bc', r"a\"bc"),
182 ('a"bc', r"a\"bc"),
195 ("a^bc", r"a\^bc"),
183 ("a^bc", r"a\^bc"),
196 ("a&bc", r"a\&bc"),
184 ("a&bc", r"a\&bc"),
197 ]
185 ]
198
186 # run the actual tests
199
187 for s1, s2 in pairs:
200 @pytest.mark.parametrize("s1,expected", pairs)
188 s1p = completer.protect_filename(s1)
201 def test_protect_filename(s1, expected):
189 assert s1p == s2
202 assert completer.protect_filename(s1) == expected
190
203
191
204
192 def check_line_split(splitter, test_specs):
205 def check_line_split(splitter, test_specs):
@@ -297,8 +310,6 class TestCompleter(unittest.TestCase):
297 self.assertIsInstance(matches, list)
310 self.assertIsInstance(matches, list)
298
311
299 def test_latex_completions(self):
312 def test_latex_completions(self):
300 from IPython.core.latex_symbols import latex_symbols
301 import random
302
313
303 ip = get_ipython()
314 ip = get_ipython()
304 # Test some random unicode symbols
315 # Test some random unicode symbols
@@ -1735,6 +1746,45 class TestCompleter(unittest.TestCase):
1735
1746
1736
1747
1737 @pytest.mark.parametrize(
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 "input, expected",
1788 "input, expected",
1739 [
1789 [
1740 ["1.234", "1.234"],
1790 ["1.234", "1.234"],
General Comments 0
You need to be logged in to leave comments. Login now