##// 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 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
@@ -347,7 +348,7 def provisionalcompleter(action='ignore'):
347 348 yield
348 349
349 350
350 def has_open_quotes(s):
351 def has_open_quotes(s: str) -> Union[str, bool]:
351 352 """Return whether a string has open quotes.
352 353
353 354 This simply counts whether the number of quote characters of either type in
@@ -368,7 +369,7 def has_open_quotes(s):
368 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 373 """Escape a string to protect certain characters."""
373 374 if set(s) & set(protectables):
374 375 if sys.platform == "win32":
@@ -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
@@ -752,7 +753,7 def completion_matcher(
752 753 priority: Optional[float] = None,
753 754 identifier: Optional[str] = None,
754 755 api_version: int = 1,
755 ):
756 ) -> Callable[[Matcher], Matcher]:
756 757 """Adds attributes describing the matcher.
757 758
758 759 Parameters
@@ -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,13 class Completer(Configurable):
1141 1142 """
1142 1143 return self._attr_matches(text)[0]
1143 1144
1144 def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]:
1145 m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer)
1145 # we simple attribute matching with normal identifiers.
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 1152 if not m2:
1147 1153 return [], ""
1148 1154 expr, attr = m2.group(1, 2)
@@ -1204,6 +1210,30 class Completer(Configurable):
1204 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 1237 def _evaluate_expr(self, expr):
1208 1238 obj = not_found
1209 1239 done = False
@@ -1225,14 +1255,14 class Completer(Configurable):
1225 1255 # e.g. user starts `(d[`, so we get `expr = '(d'`,
1226 1256 # where parenthesis is not closed.
1227 1257 # TODO: make this faster by reusing parts of the computation?
1228 expr = expr[1:]
1258 expr = self._trim_expr(expr)
1229 1259 return obj
1230 1260
1231 1261 def get__all__entries(obj):
1232 1262 """returns the strings in the __all__ attribute"""
1233 1263 try:
1234 1264 words = getattr(obj, '__all__')
1235 except:
1265 except Exception:
1236 1266 return []
1237 1267
1238 1268 return [w for w in words if isinstance(w, str)]
@@ -1447,7 +1477,7 def match_dict_keys(
1447 1477 try:
1448 1478 if not str_key.startswith(prefix_str):
1449 1479 continue
1450 except (AttributeError, TypeError, UnicodeError) as e:
1480 except (AttributeError, TypeError, UnicodeError):
1451 1481 # Python 3+ TypeError on b'a'.startswith('a') or vice-versa
1452 1482 continue
1453 1483
@@ -1495,7 +1525,7 def cursor_to_position(text:str, line:int, column:int)->int:
1495 1525 lines = text.split('\n')
1496 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 1530 def position_to_cursor(text:str, offset:int)->Tuple[int, int]:
1501 1531 """
@@ -2112,7 +2142,7 class IPCompleter(Completer):
2112 2142 result["suppress"] = is_magic_prefix and bool(result["completions"])
2113 2143 return result
2114 2144
2115 def magic_matches(self, text: str):
2145 def magic_matches(self, text: str) -> List[str]:
2116 2146 """Match magics.
2117 2147
2118 2148 .. deprecated:: 8.6
@@ -2469,7 +2499,8 class IPCompleter(Completer):
2469 2499 # parenthesis before the cursor
2470 2500 # e.g. for "foo (1+bar(x), pa<cursor>,a=1)", the candidate is "foo"
2471 2501 tokens = regexp.findall(self.text_until_cursor)
2472 iterTokens = reversed(tokens); openPar = 0
2502 iterTokens = reversed(tokens)
2503 openPar = 0
2473 2504
2474 2505 for token in iterTokens:
2475 2506 if token == ')':
@@ -2489,7 +2520,8 class IPCompleter(Completer):
2489 2520 try:
2490 2521 ids.append(next(iterTokens))
2491 2522 if not isId(ids[-1]):
2492 ids.pop(); break
2523 ids.pop()
2524 break
2493 2525 if not next(iterTokens) == '.':
2494 2526 break
2495 2527 except StopIteration:
@@ -3215,7 +3247,7 class IPCompleter(Completer):
3215 3247 else:
3216 3248 api_version = _get_matcher_api_version(matcher)
3217 3249 raise ValueError(f"Unsupported API version {api_version}")
3218 except:
3250 except BaseException:
3219 3251 # Show the ugly traceback if the matcher causes an
3220 3252 # exception, but do NOT crash the kernel!
3221 3253 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,45 +161,45 def custom_matchers(matchers):
148 161 ip.Completer.custom_matchers.clear()
149 162
150 163
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
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