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 |
|
|
453 | if "%" not in word[2:]: | |
|
453 | 454 | word = word[2:] |
|
454 | 455 | prio2 = 2 |
|
455 | 456 | elif word.startswith('%'): |
|
456 |
if |
|
|
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 |
|
|
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) |
|
|
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) |
|
|
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() |
|
|
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 |
|
|
153 | 165 |
|
|
154 | 166 |
|
@@ -183,10 +195,11 def test_protect_filename(): | |||
|
183 | 195 |
|
|
184 | 196 |
|
|
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