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 |
|
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 |
|
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 |
|
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) |
|
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) |
|
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() |
|
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,7 +161,6 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(): |
|
|||
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 |
|
198 | ||
187 | for s1, s2 in pairs: |
|
199 | ||
188 | s1p = completer.protect_filename(s1) |
|
200 | @pytest.mark.parametrize("s1,expected", pairs) | |
189 | assert s1p == s2 |
|
201 | def test_protect_filename(s1, expected): | |
|
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