Show More
@@ -178,6 +178,7 b' The suppression behaviour can is user-configurable via' | |||||
178 |
|
178 | |||
179 | from __future__ import annotations |
|
179 | from __future__ import annotations | |
180 | import builtins as builtin_mod |
|
180 | import builtins as builtin_mod | |
|
181 | import enum | |||
181 | import glob |
|
182 | import glob | |
182 | import inspect |
|
183 | import inspect | |
183 | import itertools |
|
184 | import itertools | |
@@ -186,15 +187,16 b' import os' | |||||
186 | import re |
|
187 | import re | |
187 | import string |
|
188 | import string | |
188 | import sys |
|
189 | import sys | |
|
190 | import tokenize | |||
189 | import time |
|
191 | import time | |
190 | import unicodedata |
|
192 | import unicodedata | |
191 | import uuid |
|
193 | import uuid | |
192 | import warnings |
|
194 | import warnings | |
193 | from ast import literal_eval |
|
195 | from ast import literal_eval | |
|
196 | from collections import defaultdict | |||
194 | from contextlib import contextmanager |
|
197 | from contextlib import contextmanager | |
195 | from dataclasses import dataclass |
|
198 | from dataclasses import dataclass | |
196 | from functools import cached_property, partial |
|
199 | from functools import cached_property, partial | |
197 | from importlib import import_module |
|
|||
198 | from types import SimpleNamespace |
|
200 | from types import SimpleNamespace | |
199 | from typing import ( |
|
201 | from typing import ( | |
200 | Iterable, |
|
202 | Iterable, | |
@@ -205,8 +207,6 b' from typing import (' | |||||
205 | Any, |
|
207 | Any, | |
206 | Sequence, |
|
208 | Sequence, | |
207 | Dict, |
|
209 | Dict, | |
208 | NamedTuple, |
|
|||
209 | Pattern, |
|
|||
210 | Optional, |
|
210 | Optional, | |
211 | TYPE_CHECKING, |
|
211 | TYPE_CHECKING, | |
212 | Set, |
|
212 | Set, | |
@@ -233,7 +233,6 b' from traitlets import (' | |||||
233 | Unicode, |
|
233 | Unicode, | |
234 | Dict as DictTrait, |
|
234 | Dict as DictTrait, | |
235 | Union as UnionTrait, |
|
235 | Union as UnionTrait, | |
236 | default, |
|
|||
237 | observe, |
|
236 | observe, | |
238 | ) |
|
237 | ) | |
239 | from traitlets.config.configurable import Configurable |
|
238 | from traitlets.config.configurable import Configurable | |
@@ -559,7 +558,7 b' class SimpleCompletion:' | |||||
559 |
|
558 | |||
560 | __slots__ = ["text", "type"] |
|
559 | __slots__ = ["text", "type"] | |
561 |
|
560 | |||
562 | def __init__(self, text: str, *, type: str = None): |
|
561 | def __init__(self, text: str, *, type: Optional[str] = None): | |
563 | self.text = text |
|
562 | self.text = text | |
564 | self.type = type |
|
563 | self.type = type | |
565 |
|
564 | |||
@@ -647,16 +646,18 b' MatcherResult = Union[SimpleMatcherResult, _JediMatcherResult]' | |||||
647 |
|
646 | |||
648 |
|
647 | |||
649 | class _MatcherAPIv1Base(Protocol): |
|
648 | class _MatcherAPIv1Base(Protocol): | |
650 |
def __call__(self, text: str) -> |
|
649 | def __call__(self, text: str) -> List[str]: | |
651 | """Call signature.""" |
|
650 | """Call signature.""" | |
|
651 | ... | |||
652 |
|
652 | |||
653 |
|
653 | |||
654 | class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol): |
|
654 | class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol): | |
655 | #: API version |
|
655 | #: API version | |
656 | matcher_api_version: Optional[Literal[1]] |
|
656 | matcher_api_version: Optional[Literal[1]] | |
657 |
|
657 | |||
658 |
def __call__(self, text: str) -> |
|
658 | def __call__(self, text: str) -> List[str]: | |
659 | """Call signature.""" |
|
659 | """Call signature.""" | |
|
660 | ... | |||
660 |
|
661 | |||
661 |
|
662 | |||
662 | #: Protocol describing Matcher API v1. |
|
663 | #: Protocol describing Matcher API v1. | |
@@ -671,6 +672,7 b' class MatcherAPIv2(Protocol):' | |||||
671 |
|
672 | |||
672 | def __call__(self, context: CompletionContext) -> MatcherResult: |
|
673 | def __call__(self, context: CompletionContext) -> MatcherResult: | |
673 | """Call signature.""" |
|
674 | """Call signature.""" | |
|
675 | ... | |||
674 |
|
676 | |||
675 |
|
677 | |||
676 | Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] |
|
678 | Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] | |
@@ -912,10 +914,11 b' class Completer(Configurable):' | |||||
912 | help="""Activate greedy completion. |
|
914 | help="""Activate greedy completion. | |
913 |
|
915 | |||
914 | .. deprecated:: 8.8 |
|
916 | .. deprecated:: 8.8 | |
915 | Use :any:`evaluation` instead. |
|
917 | Use :any:`evaluation` and :any:`auto_close_dict_keys` instead. | |
916 |
|
918 | |||
917 | As of IPython 8.8 proxy for ``evaluation = 'unsafe'`` when set to ``True``, |
|
919 | Whent enabled in IPython 8.8+ activates following settings for compatibility: | |
918 | and for ``'forbidden'`` when set to ``False``. |
|
920 | - ``evaluation = 'unsafe'`` | |
|
921 | - ``auto_close_dict_keys = True`` | |||
919 | """, |
|
922 | """, | |
920 | ).tag(config=True) |
|
923 | ).tag(config=True) | |
921 |
|
924 | |||
@@ -957,6 +960,11 b' class Completer(Configurable):' | |||||
957 | "Includes completion of latex commands, unicode names, and expanding " |
|
960 | "Includes completion of latex commands, unicode names, and expanding " | |
958 | "unicode characters back to latex commands.").tag(config=True) |
|
961 | "unicode characters back to latex commands.").tag(config=True) | |
959 |
|
962 | |||
|
963 | auto_close_dict_keys = Bool( | |||
|
964 | False, | |||
|
965 | help="""Enable auto-closing dictionary keys.""" | |||
|
966 | ).tag(config=True) | |||
|
967 | ||||
960 | def __init__(self, namespace=None, global_namespace=None, **kwargs): |
|
968 | def __init__(self, namespace=None, global_namespace=None, **kwargs): | |
961 | """Create a new completer for the command line. |
|
969 | """Create a new completer for the command line. | |
962 |
|
970 | |||
@@ -1119,8 +1127,80 b' def get__all__entries(obj):' | |||||
1119 | return [w for w in words if isinstance(w, str)] |
|
1127 | return [w for w in words if isinstance(w, str)] | |
1120 |
|
1128 | |||
1121 |
|
1129 | |||
1122 | def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], prefix: str, delims: str, |
|
1130 | class DictKeyState(enum.Flag): | |
1123 | extra_prefix: Optional[Tuple[Union[str, bytes], ...]]=None) -> Tuple[str, int, List[str]]: |
|
1131 | """Represent state of the key match in context of other possible matches. | |
|
1132 | ||||
|
1133 | - given `d1 = {'a': 1}` completion on `d1['<tab>` will yield `{'a': END_OF_ITEM}` as there is no tuple. | |||
|
1134 | - given `d2 = {('a', 'b'): 1}`: `d2['a', '<tab>` will yield `{'b': END_OF_TUPLE}` as there is no tuple members to add beyond `'b'`. | |||
|
1135 | - given `d3 = {('a', 'b'): 1}`: `d3['<tab>` will yield `{'a': IN_TUPLE}` as `'a'` can be added. | |||
|
1136 | - given `d4 = {'a': 1, ('a', 'b'): 2}`: `d4['<tab>` will yield `{'a': END_OF_ITEM & END_OF_TUPLE}` | |||
|
1137 | """ | |||
|
1138 | BASELINE = 0 | |||
|
1139 | END_OF_ITEM = enum.auto() | |||
|
1140 | END_OF_TUPLE = enum.auto() | |||
|
1141 | IN_TUPLE = enum.auto() | |||
|
1142 | ||||
|
1143 | ||||
|
1144 | def _parse_tokens(c): | |||
|
1145 | tokens = [] | |||
|
1146 | token_generator = tokenize.generate_tokens(iter(c.splitlines()).__next__) | |||
|
1147 | while True: | |||
|
1148 | try: | |||
|
1149 | tokens.append(next(token_generator)) | |||
|
1150 | except tokenize.TokenError: | |||
|
1151 | return tokens | |||
|
1152 | except StopIteration: | |||
|
1153 | return tokens | |||
|
1154 | ||||
|
1155 | ||||
|
1156 | def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]: | |||
|
1157 | """Match any valid Python numeric literal in a prefix of dictionary keys. | |||
|
1158 | ||||
|
1159 | References: | |||
|
1160 | - https://docs.python.org/3/reference/lexical_analysis.html#numeric-literals | |||
|
1161 | - https://docs.python.org/3/library/tokenize.html | |||
|
1162 | """ | |||
|
1163 | if prefix[-1].isspace(): | |||
|
1164 | # if user typed a space we do not have anything to complete | |||
|
1165 | # even if there was a valid number token before | |||
|
1166 | return None | |||
|
1167 | tokens = _parse_tokens(prefix) | |||
|
1168 | rev_tokens = reversed(tokens) | |||
|
1169 | skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} | |||
|
1170 | number = None | |||
|
1171 | for token in rev_tokens: | |||
|
1172 | if token.type in skip_over: | |||
|
1173 | continue | |||
|
1174 | if number is None: | |||
|
1175 | if token.type == tokenize.NUMBER: | |||
|
1176 | number = token.string | |||
|
1177 | continue | |||
|
1178 | else: | |||
|
1179 | # we did not match a number | |||
|
1180 | return None | |||
|
1181 | if token.type == tokenize.OP: | |||
|
1182 | if token.string == ',': | |||
|
1183 | break | |||
|
1184 | if token.string in {'+', '-'}: | |||
|
1185 | number = token.string + number | |||
|
1186 | else: | |||
|
1187 | return None | |||
|
1188 | return number | |||
|
1189 | ||||
|
1190 | ||||
|
1191 | _INT_FORMATS = { | |||
|
1192 | '0b': bin, | |||
|
1193 | '0o': oct, | |||
|
1194 | '0x': hex, | |||
|
1195 | } | |||
|
1196 | ||||
|
1197 | ||||
|
1198 | def match_dict_keys( | |||
|
1199 | keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], | |||
|
1200 | prefix: str, | |||
|
1201 | delims: str, | |||
|
1202 | extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None | |||
|
1203 | ) -> Tuple[str, int, Dict[str, DictKeyState]]: | |||
1124 | """Used by dict_key_matches, matching the prefix to a list of keys |
|
1204 | """Used by dict_key_matches, matching the prefix to a list of keys | |
1125 |
|
1205 | |||
1126 | Parameters |
|
1206 | Parameters | |
@@ -1140,16 +1220,21 b' def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]]' | |||||
1140 | A tuple of three elements: ``quote``, ``token_start``, ``matched``, with |
|
1220 | A tuple of three elements: ``quote``, ``token_start``, ``matched``, with | |
1141 | ``quote`` being the quote that need to be used to close current string. |
|
1221 | ``quote`` being the quote that need to be used to close current string. | |
1142 | ``token_start`` the position where the replacement should start occurring, |
|
1222 | ``token_start`` the position where the replacement should start occurring, | |
1143 |
``matches`` a |
|
1223 | ``matches`` a dictionary of replacement/completion keys on keys and values | |
1144 |
|
1224 | indicating whether the state. | ||
1145 | """ |
|
1225 | """ | |
1146 | prefix_tuple = extra_prefix if extra_prefix else () |
|
1226 | prefix_tuple = extra_prefix if extra_prefix else () | |
1147 |
|
1227 | |||
1148 | Nprefix = len(prefix_tuple) |
|
1228 | prefix_tuple_size = sum([ | |
|
1229 | # for pandas, do not count slices as taking space | |||
|
1230 | not isinstance(k, slice) | |||
|
1231 | for k in prefix_tuple | |||
|
1232 | ]) | |||
1149 | text_serializable_types = (str, bytes, int, float, slice) |
|
1233 | text_serializable_types = (str, bytes, int, float, slice) | |
|
1234 | ||||
1150 | def filter_prefix_tuple(key): |
|
1235 | def filter_prefix_tuple(key): | |
1151 | # Reject too short keys |
|
1236 | # Reject too short keys | |
1152 |
if len(key) <= |
|
1237 | if len(key) <= prefix_tuple_size: | |
1153 | return False |
|
1238 | return False | |
1154 | # Reject keys which cannot be serialised to text |
|
1239 | # Reject keys which cannot be serialised to text | |
1155 | for k in key: |
|
1240 | for k in key: | |
@@ -1162,28 +1247,58 b' def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]]' | |||||
1162 | # All checks passed! |
|
1247 | # All checks passed! | |
1163 | return True |
|
1248 | return True | |
1164 |
|
1249 | |||
1165 |
filtered_keys: |
|
1250 | filtered_key_is_final: Dict[Union[str, bytes, int, float], DictKeyState] = defaultdict(lambda: DictKeyState.BASELINE) | |
1166 |
|
||||
1167 | def _add_to_filtered_keys(key): |
|
|||
1168 | if isinstance(key, text_serializable_types): |
|
|||
1169 | filtered_keys.append(key) |
|
|||
1170 |
|
1251 | |||
1171 | for k in keys: |
|
1252 | for k in keys: | |
|
1253 | # If at least one of the matches is not final, mark as undetermined. | |||
|
1254 | # This can happen with `d = {111: 'b', (111, 222): 'a'}` where | |||
|
1255 | # `111` appears final on first match but is not final on the second. | |||
|
1256 | ||||
1172 | if isinstance(k, tuple): |
|
1257 | if isinstance(k, tuple): | |
1173 | if filter_prefix_tuple(k): |
|
1258 | if filter_prefix_tuple(k): | |
1174 | _add_to_filtered_keys(k[Nprefix]) |
|
1259 | key_fragment = k[prefix_tuple_size] | |
|
1260 | filtered_key_is_final[key_fragment] |= ( | |||
|
1261 | DictKeyState.END_OF_TUPLE | |||
|
1262 | if len(k) == prefix_tuple_size + 1 else | |||
|
1263 | DictKeyState.IN_TUPLE | |||
|
1264 | ) | |||
|
1265 | elif prefix_tuple_size > 0: | |||
|
1266 | # we are completing a tuple but this key is not a tuple, | |||
|
1267 | # so we should ignore it | |||
|
1268 | pass | |||
1175 | else: |
|
1269 | else: | |
1176 | _add_to_filtered_keys(k) |
|
1270 | if isinstance(k, text_serializable_types): | |
|
1271 | filtered_key_is_final[k] |= DictKeyState.END_OF_ITEM | |||
|
1272 | ||||
|
1273 | filtered_keys = filtered_key_is_final.keys() | |||
1177 |
|
1274 | |||
1178 | if not prefix: |
|
1275 | if not prefix: | |
1179 |
return '', 0, |
|
1276 | return '', 0, {repr(k): v for k, v in filtered_key_is_final.items()} | |
1180 | quote_match = re.search('["\']', prefix) |
|
1277 | ||
1181 | assert quote_match is not None # silence mypy |
|
1278 | quote_match = re.search('(?:"|\')', prefix) | |
1182 | quote = quote_match.group() |
|
1279 | is_user_prefix_numeric = False | |
1183 | try: |
|
1280 | ||
1184 | prefix_str = literal_eval(prefix + quote) |
|
1281 | if quote_match: | |
1185 | except Exception: |
|
1282 | quote = quote_match.group() | |
1186 | return '', 0, [] |
|
1283 | valid_prefix = prefix + quote | |
|
1284 | try: | |||
|
1285 | prefix_str = literal_eval(valid_prefix) | |||
|
1286 | except Exception: | |||
|
1287 | return '', 0, {} | |||
|
1288 | else: | |||
|
1289 | # If it does not look like a string, let's assume | |||
|
1290 | # we are dealing with a number or variable. | |||
|
1291 | number_match = _match_number_in_dict_key_prefix(prefix) | |||
|
1292 | ||||
|
1293 | # We do not want the key matcher to suggest variable names so we yield: | |||
|
1294 | if number_match is None: | |||
|
1295 | # The alternative would be to assume that user forgort the quote | |||
|
1296 | # and if the substring matches, suggest adding it at the start. | |||
|
1297 | return '', 0, {} | |||
|
1298 | ||||
|
1299 | prefix_str = number_match | |||
|
1300 | is_user_prefix_numeric = True | |||
|
1301 | quote = '' | |||
1187 |
|
1302 | |||
1188 | pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' |
|
1303 | pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' | |
1189 | token_match = re.search(pattern, prefix, re.UNICODE) |
|
1304 | token_match = re.search(pattern, prefix, re.UNICODE) | |
@@ -1191,13 +1306,29 b' def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]]' | |||||
1191 | token_start = token_match.start() |
|
1306 | token_start = token_match.start() | |
1192 | token_prefix = token_match.group() |
|
1307 | token_prefix = token_match.group() | |
1193 |
|
1308 | |||
1194 |
matched: |
|
1309 | matched: Dict[str, DictKeyState] = {} | |
|
1310 | ||||
1195 | for key in filtered_keys: |
|
1311 | for key in filtered_keys: | |
1196 |
|
|
1312 | if isinstance(key, (int, float)): | |
|
1313 | # User typed a number but this key is not a number. | |||
|
1314 | if not is_user_prefix_numeric: | |||
|
1315 | continue | |||
|
1316 | str_key = str(key) | |||
|
1317 | if isinstance(key, int): | |||
|
1318 | int_base = prefix_str[:2].lower() | |||
|
1319 | # if user typed integer using binary/oct/hex notation: | |||
|
1320 | if int_base in _INT_FORMATS: | |||
|
1321 | int_format = _INT_FORMATS[int_base] | |||
|
1322 | str_key = int_format(key) | |||
|
1323 | else: | |||
|
1324 | # User typed a string but this key is a number. | |||
|
1325 | if is_user_prefix_numeric: | |||
|
1326 | continue | |||
|
1327 | str_key = key | |||
1197 | try: |
|
1328 | try: | |
1198 | if not str_key.startswith(prefix_str): |
|
1329 | if not str_key.startswith(prefix_str): | |
1199 | continue |
|
1330 | continue | |
1200 | except (AttributeError, TypeError, UnicodeError): |
|
1331 | except (AttributeError, TypeError, UnicodeError) as e: | |
1201 | # Python 3+ TypeError on b'a'.startswith('a') or vice-versa |
|
1332 | # Python 3+ TypeError on b'a'.startswith('a') or vice-versa | |
1202 | continue |
|
1333 | continue | |
1203 |
|
1334 | |||
@@ -1213,7 +1344,9 b' def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]]' | |||||
1213 | rem_repr = rem_repr.replace('"', '\\"') |
|
1344 | rem_repr = rem_repr.replace('"', '\\"') | |
1214 |
|
1345 | |||
1215 | # then reinsert prefix from start of token |
|
1346 | # then reinsert prefix from start of token | |
1216 |
match |
|
1347 | match = '%s%s' % (token_prefix, rem_repr) | |
|
1348 | ||||
|
1349 | matched[match] = filtered_key_is_final[key] | |||
1217 | return quote, token_start, matched |
|
1350 | return quote, token_start, matched | |
1218 |
|
1351 | |||
1219 |
|
1352 | |||
@@ -1447,24 +1580,39 b' DICT_MATCHER_REGEX = re.compile(r"""(?x)' | |||||
1447 | \s* # and optional whitespace |
|
1580 | \s* # and optional whitespace | |
1448 | # Capture any number of serializable objects (e.g. "a", "b", 'c') |
|
1581 | # Capture any number of serializable objects (e.g. "a", "b", 'c') | |
1449 | # and slices |
|
1582 | # and slices | |
1450 | ((?:[uUbB]? # string prefix (r not handled) |
|
1583 | ((?:(?: | |
1451 | (?: |
|
1584 | (?: # closed string | |
1452 | '(?:[^']|(?<!\\)\\')*' |
|
1585 | [uUbB]? # string prefix (r not handled) | |
1453 | | |
|
1586 | (?: | |
1454 |
|
|
1587 | '(?:[^']|(?<!\\)\\')*' | |
|
1588 | | | |||
|
1589 | "(?:[^"]|(?<!\\)\\")*" | |||
|
1590 | ) | |||
|
1591 | ) | |||
1455 | | |
|
1592 | | | |
1456 | # capture integers and slices |
|
1593 | # capture integers and slices | |
1457 | (?:[-+]?\d+)?(?::(?:[-+]?\d+)?){0,2} |
|
1594 | (?:[-+]?\d+)?(?::(?:[-+]?\d+)?){0,2} | |
|
1595 | | | |||
|
1596 | # integer in bin/hex/oct notation | |||
|
1597 | 0[bBxXoO]_?(?:\w|\d)+ | |||
1458 | ) |
|
1598 | ) | |
1459 | \s*,\s* |
|
1599 | \s*,\s* | |
1460 | )*) |
|
1600 | )*) | |
1461 | ([uUbB]? # string prefix (r not handled) |
|
1601 | ((?: | |
1462 |
(?: |
|
1602 | (?: # unclosed string | |
1463 | '(?:[^']|(?<!\\)\\')* |
|
1603 | [uUbB]? # string prefix (r not handled) | |
1464 | | |
|
1604 | (?: | |
1465 |
|
|
1605 | '(?:[^']|(?<!\\)\\')* | |
|
1606 | | | |||
|
1607 | "(?:[^"]|(?<!\\)\\")* | |||
|
1608 | ) | |||
|
1609 | ) | |||
1466 | | |
|
1610 | | | |
|
1611 | # unfinished integer | |||
1467 | (?:[-+]?\d+) |
|
1612 | (?:[-+]?\d+) | |
|
1613 | | | |||
|
1614 | # integer in bin/hex/oct notation | |||
|
1615 | 0[bBxXoO]_?(?:\w|\d)+ | |||
1468 | ) |
|
1616 | ) | |
1469 | )? |
|
1617 | )? | |
1470 | $ |
|
1618 | $ | |
@@ -1473,7 +1621,7 b' $' | |||||
1473 | def _convert_matcher_v1_result_to_v2( |
|
1621 | def _convert_matcher_v1_result_to_v2( | |
1474 | matches: Sequence[str], |
|
1622 | matches: Sequence[str], | |
1475 | type: str, |
|
1623 | type: str, | |
1476 | fragment: str = None, |
|
1624 | fragment: Optional[str] = None, | |
1477 | suppress_if_matches: bool = False, |
|
1625 | suppress_if_matches: bool = False, | |
1478 | ) -> SimpleMatcherResult: |
|
1626 | ) -> SimpleMatcherResult: | |
1479 | """Utility to help with transition""" |
|
1627 | """Utility to help with transition""" | |
@@ -1494,9 +1642,11 b' class IPCompleter(Completer):' | |||||
1494 | """update the splitter and readline delims when greedy is changed""" |
|
1642 | """update the splitter and readline delims when greedy is changed""" | |
1495 | if change['new']: |
|
1643 | if change['new']: | |
1496 | self.evaluation = 'unsafe' |
|
1644 | self.evaluation = 'unsafe' | |
|
1645 | self.auto_close_dict_keys = True | |||
1497 | self.splitter.delims = GREEDY_DELIMS |
|
1646 | self.splitter.delims = GREEDY_DELIMS | |
1498 | else: |
|
1647 | else: | |
1499 | self.evaluation = 'limitted' |
|
1648 | self.evaluation = 'limitted' | |
|
1649 | self.auto_close_dict_keys = False | |||
1500 | self.splitter.delims = DELIMS |
|
1650 | self.splitter.delims = DELIMS | |
1501 |
|
1651 | |||
1502 | dict_keys_only = Bool( |
|
1652 | dict_keys_only = Bool( | |
@@ -2294,7 +2444,7 b' class IPCompleter(Completer):' | |||||
2294 | extra_prefix=tuple_prefix |
|
2444 | extra_prefix=tuple_prefix | |
2295 | ) |
|
2445 | ) | |
2296 | if not matches: |
|
2446 | if not matches: | |
2297 |
return |
|
2447 | return [] | |
2298 |
|
2448 | |||
2299 | # get the cursor position of |
|
2449 | # get the cursor position of | |
2300 | # - the text being completed |
|
2450 | # - the text being completed | |
@@ -2313,26 +2463,55 b' class IPCompleter(Completer):' | |||||
2313 | else: |
|
2463 | else: | |
2314 | leading = text[text_start:completion_start] |
|
2464 | leading = text[text_start:completion_start] | |
2315 |
|
2465 | |||
2316 | # the index of the `[` character |
|
|||
2317 | bracket_idx = match.end(1) |
|
|||
2318 |
|
||||
2319 | # append closing quote and bracket as appropriate |
|
2466 | # append closing quote and bracket as appropriate | |
2320 | # this is *not* appropriate if the opening quote or bracket is outside |
|
2467 | # this is *not* appropriate if the opening quote or bracket is outside | |
2321 | # the text given to this method |
|
2468 | # the text given to this method, e.g. `d["""a\nt | |
2322 | suf = '' |
|
2469 | can_close_quote = False | |
2323 | continuation = self.line_buffer[len(self.text_until_cursor):] |
|
2470 | can_close_bracket = False | |
2324 | if key_start > text_start and closing_quote: |
|
2471 | ||
2325 | # quotes were opened inside text, maybe close them |
|
2472 | continuation = self.line_buffer[len(self.text_until_cursor):].strip() | |
2326 | if continuation.startswith(closing_quote): |
|
2473 | ||
2327 |
|
|
2474 | if continuation.startswith(closing_quote): | |
2328 | else: |
|
2475 | # do not close if already closed, e.g. `d['a<tab>'` | |
2329 | suf += closing_quote |
|
2476 | continuation = continuation[len(closing_quote):] | |
2330 | if bracket_idx > text_start: |
|
2477 | else: | |
2331 | # brackets were opened inside text, maybe close them |
|
2478 | can_close_quote = True | |
2332 | if not continuation.startswith(']'): |
|
2479 | ||
2333 | suf += ']' |
|
2480 | continuation = continuation.strip() | |
|
2481 | ||||
|
2482 | # e.g. `pandas.DataFrame` has different tuple indexer behaviour, | |||
|
2483 | # handling it is out of scope, so let's avoid appending suffixes. | |||
|
2484 | has_known_tuple_handling = isinstance(obj, dict) | |||
2334 |
|
2485 | |||
2335 | return [leading + k + suf for k in matches] |
|
2486 | can_close_bracket = not continuation.startswith(']') and self.auto_close_dict_keys | |
|
2487 | can_close_tuple_item = not continuation.startswith(',') and has_known_tuple_handling and self.auto_close_dict_keys | |||
|
2488 | can_close_quote = can_close_quote and self.auto_close_dict_keys | |||
|
2489 | ||||
|
2490 | # fast path if closing qoute should be appended but not suffix is allowed | |||
|
2491 | if not can_close_quote and not can_close_bracket and closing_quote: | |||
|
2492 | return [leading + k for k in matches] | |||
|
2493 | ||||
|
2494 | results = [] | |||
|
2495 | ||||
|
2496 | end_of_tuple_or_item = DictKeyState.END_OF_TUPLE | DictKeyState.END_OF_ITEM | |||
|
2497 | ||||
|
2498 | for k, state_flag in matches.items(): | |||
|
2499 | result = leading + k | |||
|
2500 | if can_close_quote and closing_quote: | |||
|
2501 | result += closing_quote | |||
|
2502 | ||||
|
2503 | if state_flag == end_of_tuple_or_item: | |||
|
2504 | # We do not know which suffix to add, | |||
|
2505 | # e.g. both tuple item and string | |||
|
2506 | # match this item. | |||
|
2507 | pass | |||
|
2508 | ||||
|
2509 | if state_flag in end_of_tuple_or_item and can_close_bracket: | |||
|
2510 | result += ']' | |||
|
2511 | if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: | |||
|
2512 | result += ', ' | |||
|
2513 | results.append(result) | |||
|
2514 | return results | |||
2336 |
|
2515 | |||
2337 | @context_matcher() |
|
2516 | @context_matcher() | |
2338 | def unicode_name_matcher(self, context: CompletionContext): |
|
2517 | def unicode_name_matcher(self, context: CompletionContext): |
@@ -1,11 +1,19 b'' | |||||
1 |
from typing import Callable |
|
1 | from typing import Callable, Set, Tuple, NamedTuple, Literal, Union, TYPE_CHECKING | |
2 | import collections |
|
2 | import collections | |
3 | import sys |
|
3 | import sys | |
4 | import ast |
|
4 | import ast | |
5 | import types |
|
|||
6 | from functools import cached_property |
|
5 | from functools import cached_property | |
7 | from dataclasses import dataclass, field |
|
6 | from dataclasses import dataclass, field | |
8 |
|
7 | |||
|
8 | from IPython.utils.docs import GENERATING_DOCUMENTATION | |||
|
9 | ||||
|
10 | ||||
|
11 | if TYPE_CHECKING or GENERATING_DOCUMENTATION: | |||
|
12 | from typing_extensions import Protocol | |||
|
13 | else: | |||
|
14 | # do not require on runtime | |||
|
15 | Protocol = object # requires Python >=3.8 | |||
|
16 | ||||
9 |
|
17 | |||
10 | class HasGetItem(Protocol): |
|
18 | class HasGetItem(Protocol): | |
11 | def __getitem__(self, key) -> None: ... |
|
19 | def __getitem__(self, key) -> None: ... | |
@@ -266,20 +274,23 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
266 | Currently does not support evaluation of functions with arguments. |
|
274 | Currently does not support evaluation of functions with arguments. | |
267 |
|
275 | |||
268 | Does not evaluate actions which always have side effects: |
|
276 | Does not evaluate actions which always have side effects: | |
269 | - class definitions (`class sth: ...`) |
|
277 | - class definitions (``class sth: ...``) | |
270 | - function definitions (`def sth: ...`) |
|
278 | - function definitions (``def sth: ...``) | |
271 | - variable assignments (`x = 1`) |
|
279 | - variable assignments (``x = 1``) | |
272 | - augumented assignments (`x += 1`) |
|
280 | - augumented assignments (``x += 1``) | |
273 | - deletions (`del x`) |
|
281 | - deletions (``del x``) | |
274 |
|
282 | |||
275 | Does not evaluate operations which do not return values: |
|
283 | Does not evaluate operations which do not return values: | |
276 | - assertions (`assert x`) |
|
284 | - assertions (``assert x``) | |
277 | - pass (`pass`) |
|
285 | - pass (``pass``) | |
278 | - imports (`import x`) |
|
286 | - imports (``import x``) | |
279 | - control flow |
|
287 | - control flow | |
280 | - conditionals (`if x:`) except for terenary IfExp (`a if x else b`) |
|
288 | - conditionals (``if x:``) except for terenary IfExp (``a if x else b``) | |
281 | - loops (`for` and `while`) |
|
289 | - loops (``for`` and `while``) | |
282 | - exception handling |
|
290 | - exception handling | |
|
291 | ||||
|
292 | The purpose of this function is to guard against unwanted side-effects; | |||
|
293 | it does not give guarantees on protection from malicious code execution. | |||
283 | """ |
|
294 | """ | |
284 | policy = EVALUATION_POLICIES[context.evaluation] |
|
295 | policy = EVALUATION_POLICIES[context.evaluation] | |
285 | if node is None: |
|
296 | if node is None: |
@@ -24,6 +24,7 b' from IPython.core.completer import (' | |||||
24 | provisionalcompleter, |
|
24 | provisionalcompleter, | |
25 | match_dict_keys, |
|
25 | match_dict_keys, | |
26 | _deduplicate_completions, |
|
26 | _deduplicate_completions, | |
|
27 | _match_number_in_dict_key_prefix, | |||
27 | completion_matcher, |
|
28 | completion_matcher, | |
28 | SimpleCompletion, |
|
29 | SimpleCompletion, | |
29 | CompletionContext, |
|
30 | CompletionContext, | |
@@ -181,7 +182,6 b' def check_line_split(splitter, test_specs):' | |||||
181 | out = splitter.split_line(line, cursor_pos) |
|
182 | out = splitter.split_line(line, cursor_pos) | |
182 | assert out == split |
|
183 | assert out == split | |
183 |
|
184 | |||
184 |
|
||||
185 | def test_line_split(): |
|
185 | def test_line_split(): | |
186 | """Basic line splitter test with default specs.""" |
|
186 | """Basic line splitter test with default specs.""" | |
187 | sp = completer.CompletionSplitter() |
|
187 | sp = completer.CompletionSplitter() | |
@@ -852,16 +852,37 b' class TestCompleter(unittest.TestCase):' | |||||
852 | """ |
|
852 | """ | |
853 | delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" |
|
853 | delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" | |
854 |
|
854 | |||
|
855 | def match(*args, **kwargs): | |||
|
856 | quote, offset, matches = match_dict_keys(*args, **kwargs) | |||
|
857 | return quote, offset, list(matches) | |||
|
858 | ||||
855 | keys = ["foo", b"far"] |
|
859 | keys = ["foo", b"far"] | |
856 |
assert match |
|
860 | assert match(keys, "b'", delims=delims) == ("'", 2, ["far"]) | |
857 |
assert match |
|
861 | assert match(keys, "b'f", delims=delims) == ("'", 2, ["far"]) | |
858 |
assert match |
|
862 | assert match(keys, 'b"', delims=delims) == ('"', 2, ["far"]) | |
859 |
assert match |
|
863 | assert match(keys, 'b"f', delims=delims) == ('"', 2, ["far"]) | |
860 |
|
864 | |||
861 |
assert match |
|
865 | assert match(keys, "'", delims=delims) == ("'", 1, ["foo"]) | |
862 |
assert match |
|
866 | assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) | |
863 |
assert match |
|
867 | assert match(keys, '"', delims=delims) == ('"', 1, ["foo"]) | |
864 |
assert match |
|
868 | assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) | |
|
869 | ||||
|
870 | # Completion on first item of tuple | |||
|
871 | keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] | |||
|
872 | assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) | |||
|
873 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) | |||
|
874 | ||||
|
875 | # Completion on numbers | |||
|
876 | keys = [ | |||
|
877 | 0xdeadbeef, # 3735928559 | |||
|
878 | 1111, 1234, "1999", | |||
|
879 | 0b10101, # 21 | |||
|
880 | 22 | |||
|
881 | ] | |||
|
882 | assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) | |||
|
883 | assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) | |||
|
884 | assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) | |||
|
885 | assert match(keys, "0b101", delims=delims) == ("", 0, ['0b10101', '0b10110']) | |||
865 |
|
886 | |||
866 | def test_match_dict_keys_tuple(self): |
|
887 | def test_match_dict_keys_tuple(self): | |
867 | """ |
|
888 | """ | |
@@ -872,30 +893,85 b' class TestCompleter(unittest.TestCase):' | |||||
872 |
|
893 | |||
873 | keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] |
|
894 | keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] | |
874 |
|
895 | |||
|
896 | def match(*args, **kwargs): | |||
|
897 | quote, offset, matches = match_dict_keys(*args, **kwargs) | |||
|
898 | return quote, offset, list(matches) | |||
|
899 | ||||
875 | # Completion on first key == "foo" |
|
900 | # Completion on first key == "foo" | |
876 |
assert match |
|
901 | assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) | |
877 |
assert match |
|
902 | assert match(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) | |
878 |
assert match |
|
903 | assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) | |
879 |
assert match |
|
904 | assert match(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) | |
880 |
assert match |
|
905 | assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) | |
881 |
assert match |
|
906 | assert match(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) | |
882 |
assert match |
|
907 | assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) | |
883 |
assert match |
|
908 | assert match(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) | |
884 |
|
909 | |||
885 | # No Completion |
|
910 | # No Completion | |
886 |
assert match |
|
911 | assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) | |
887 |
assert match |
|
912 | assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) | |
888 |
|
913 | |||
889 | keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] |
|
914 | keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] | |
890 |
assert match |
|
915 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2"]) | |
891 |
assert match |
|
916 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) | |
892 |
assert match |
|
917 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) | |
893 |
assert match |
|
918 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) | |
|
919 | ||||
|
920 | keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] | |||
|
921 | assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["2222"]) | |||
|
922 | assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ("", 0, ["1111", "'2222'"]) | |||
|
923 | assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar"]) | |||
|
924 | assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ("", 0, ["'bar'", "4444"]) | |||
|
925 | assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) | |||
|
926 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) | |||
|
927 | ||||
|
928 | def test_dict_key_completion_closures(self): | |||
|
929 | ip = get_ipython() | |||
|
930 | complete = ip.Completer.complete | |||
|
931 | ip.Completer.auto_close_dict_keys = True | |||
894 |
|
932 | |||
895 | keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] |
|
933 | ip.user_ns["d"] = { | |
896 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["1111", "2222"]) |
|
934 | # tuple only | |
897 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar", "test"]) |
|
935 | ('aa', 11): None, | |
898 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) |
|
936 | # tuple and non-tuple | |
|
937 | ('bb', 22): None, | |||
|
938 | 'bb': None, | |||
|
939 | # non-tuple only | |||
|
940 | 'cc': None, | |||
|
941 | # numeric tuple only | |||
|
942 | (77, 'x'): None, | |||
|
943 | # numeric tuple and non-tuple | |||
|
944 | (88, 'y'): None, | |||
|
945 | 88: None, | |||
|
946 | # numeric non-tuple only | |||
|
947 | 99: None, | |||
|
948 | } | |||
|
949 | ||||
|
950 | _, matches = complete(line_buffer="d[") | |||
|
951 | # should append `, ` if matches a tuple only | |||
|
952 | self.assertIn("'aa', ", matches) | |||
|
953 | # should not append anything if matches a tuple and an item | |||
|
954 | self.assertIn("'bb'", matches) | |||
|
955 | # should append `]` if matches and item only | |||
|
956 | self.assertIn("'cc']", matches) | |||
|
957 | ||||
|
958 | # should append `, ` if matches a tuple only | |||
|
959 | self.assertIn("77, ", matches) | |||
|
960 | # should not append anything if matches a tuple and an item | |||
|
961 | self.assertIn("88", matches) | |||
|
962 | # should append `]` if matches and item only | |||
|
963 | self.assertIn("99]", matches) | |||
|
964 | ||||
|
965 | _, matches = complete(line_buffer="d['aa', ") | |||
|
966 | # should restrict matches to those matching tuple prefix | |||
|
967 | self.assertIn("11]", matches) | |||
|
968 | self.assertNotIn("'bb'", matches) | |||
|
969 | self.assertNotIn("'bb', ", matches) | |||
|
970 | self.assertNotIn("'bb']", matches) | |||
|
971 | self.assertNotIn("'cc'", matches) | |||
|
972 | self.assertNotIn("'cc', ", matches) | |||
|
973 | self.assertNotIn("'cc']", matches) | |||
|
974 | ip.Completer.auto_close_dict_keys = False | |||
899 |
|
975 | |||
900 | def test_dict_key_completion_string(self): |
|
976 | def test_dict_key_completion_string(self): | |
901 | """Test dictionary key completion for string keys""" |
|
977 | """Test dictionary key completion for string keys""" | |
@@ -1052,6 +1128,35 b' class TestCompleter(unittest.TestCase):' | |||||
1052 | self.assertNotIn("foo", matches) |
|
1128 | self.assertNotIn("foo", matches) | |
1053 | self.assertNotIn("bar", matches) |
|
1129 | self.assertNotIn("bar", matches) | |
1054 |
|
1130 | |||
|
1131 | def test_dict_key_completion_numbers(self): | |||
|
1132 | ip = get_ipython() | |||
|
1133 | complete = ip.Completer.complete | |||
|
1134 | ||||
|
1135 | ip.user_ns["d"] = { | |||
|
1136 | 0xdeadbeef: None, # 3735928559 | |||
|
1137 | 1111: None, | |||
|
1138 | 1234: None, | |||
|
1139 | "1999": None, | |||
|
1140 | 0b10101: None, # 21 | |||
|
1141 | 22: None | |||
|
1142 | } | |||
|
1143 | _, matches = complete(line_buffer="d[1") | |||
|
1144 | self.assertIn("1111", matches) | |||
|
1145 | self.assertIn("1234", matches) | |||
|
1146 | self.assertNotIn("1999", matches) | |||
|
1147 | self.assertNotIn("'1999'", matches) | |||
|
1148 | ||||
|
1149 | _, matches = complete(line_buffer="d[0xdead") | |||
|
1150 | self.assertIn("0xdeadbeef", matches) | |||
|
1151 | ||||
|
1152 | _, matches = complete(line_buffer="d[2") | |||
|
1153 | self.assertIn("21", matches) | |||
|
1154 | self.assertIn("22", matches) | |||
|
1155 | ||||
|
1156 | _, matches = complete(line_buffer="d[0b101") | |||
|
1157 | self.assertIn("0b10101", matches) | |||
|
1158 | self.assertIn("0b10110", matches) | |||
|
1159 | ||||
1055 | def test_dict_key_completion_contexts(self): |
|
1160 | def test_dict_key_completion_contexts(self): | |
1056 | """Test expression contexts in which dict key completion occurs""" |
|
1161 | """Test expression contexts in which dict key completion occurs""" | |
1057 | ip = get_ipython() |
|
1162 | ip = get_ipython() | |
@@ -1545,3 +1650,35 b' class TestCompleter(unittest.TestCase):' | |||||
1545 | _(["completion_b"]) |
|
1650 | _(["completion_b"]) | |
1546 | a_matcher.matcher_priority = 3 |
|
1651 | a_matcher.matcher_priority = 3 | |
1547 | _(["completion_a"]) |
|
1652 | _(["completion_a"]) | |
|
1653 | ||||
|
1654 | ||||
|
1655 | @pytest.mark.parametrize( | |||
|
1656 | 'input, expected', | |||
|
1657 | [ | |||
|
1658 | ['1.234', '1.234'], | |||
|
1659 | # should match signed numbers | |||
|
1660 | ['+1', '+1'], | |||
|
1661 | ['-1', '-1'], | |||
|
1662 | ['-1.0', '-1.0'], | |||
|
1663 | ['-1.', '-1.'], | |||
|
1664 | ['+1.', '+1.'], | |||
|
1665 | ['.1', '.1'], | |||
|
1666 | # should not match non-numbers | |||
|
1667 | ['1..', None], | |||
|
1668 | ['..', None], | |||
|
1669 | ['.1.', None], | |||
|
1670 | # should match after comma | |||
|
1671 | [',1', '1'], | |||
|
1672 | [', 1', '1'], | |||
|
1673 | [', .1', '.1'], | |||
|
1674 | [', +.1', '+.1'], | |||
|
1675 | # should not match after trailing spaces | |||
|
1676 | ['.1 ', None], | |||
|
1677 | # some complex cases | |||
|
1678 | ['0b_0011_1111_0100_1110', '0b_0011_1111_0100_1110'], | |||
|
1679 | ['0xdeadbeef', '0xdeadbeef'], | |||
|
1680 | ['0b_1110_0101', '0b_1110_0101'] | |||
|
1681 | ] | |||
|
1682 | ) | |||
|
1683 | def test_match_numeric_literal_for_dict_key(input, expected): | |||
|
1684 | assert _match_number_in_dict_key_prefix(input) == expected |
General Comments 0
You need to be logged in to leave comments.
Login now