Show More
@@ -923,8 +923,8 b' class Completer(Configurable):' | |||||
923 | ).tag(config=True) |
|
923 | ).tag(config=True) | |
924 |
|
924 | |||
925 | evaluation = Enum( |
|
925 | evaluation = Enum( | |
926 |
( |
|
926 | ("forbidden", "minimal", "limitted", "unsafe", "dangerous"), | |
927 |
default_value= |
|
927 | default_value="limitted", | |
928 | help="""Code evaluation under completion. |
|
928 | help="""Code evaluation under completion. | |
929 |
|
929 | |||
930 | Successive options allow to enable more eager evaluation for more accurate completion suggestions, |
|
930 | Successive options allow to enable more eager evaluation for more accurate completion suggestions, | |
@@ -961,8 +961,7 b' class Completer(Configurable):' | |||||
961 | "unicode characters back to latex commands.").tag(config=True) |
|
961 | "unicode characters back to latex commands.").tag(config=True) | |
962 |
|
962 | |||
963 | auto_close_dict_keys = Bool( |
|
963 | auto_close_dict_keys = Bool( | |
964 | False, |
|
964 | False, help="""Enable auto-closing dictionary keys.""" | |
965 | help="""Enable auto-closing dictionary keys.""" |
|
|||
966 | ).tag(config=True) |
|
965 | ).tag(config=True) | |
967 |
|
966 | |||
968 | def __init__(self, namespace=None, global_namespace=None, **kwargs): |
|
967 | def __init__(self, namespace=None, global_namespace=None, **kwargs): | |
@@ -1066,7 +1065,7 b' class Completer(Configurable):' | |||||
1066 | m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) |
|
1065 | m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) | |
1067 | if not m2: |
|
1066 | if not m2: | |
1068 | return [] |
|
1067 | return [] | |
1069 | expr, attr = m2.group(1,2) |
|
1068 | expr, attr = m2.group(1, 2) | |
1070 |
|
1069 | |||
1071 | obj = self._evaluate_expr(expr) |
|
1070 | obj = self._evaluate_expr(expr) | |
1072 |
|
1071 | |||
@@ -1090,8 +1089,7 b' class Completer(Configurable):' | |||||
1090 | pass |
|
1089 | pass | |
1091 | # Build match list to return |
|
1090 | # Build match list to return | |
1092 | n = len(attr) |
|
1091 | n = len(attr) | |
1093 |
return ["%s.%s" % (expr, w) for w in words if w[:n] == attr |
|
1092 | return ["%s.%s" % (expr, w) for w in words if w[:n] == attr] | |
1094 |
|
||||
1095 |
|
1093 | |||
1096 | def _evaluate_expr(self, expr): |
|
1094 | def _evaluate_expr(self, expr): | |
1097 | obj = not_found |
|
1095 | obj = not_found | |
@@ -1103,13 +1101,13 b' class Completer(Configurable):' | |||||
1103 | EvaluationContext( |
|
1101 | EvaluationContext( | |
1104 | globals_=self.global_namespace, |
|
1102 | globals_=self.global_namespace, | |
1105 | locals_=self.namespace, |
|
1103 | locals_=self.namespace, | |
1106 | evaluation=self.evaluation |
|
1104 | evaluation=self.evaluation, | |
1107 | ) |
|
1105 | ), | |
1108 | ) |
|
1106 | ) | |
1109 | done = True |
|
1107 | done = True | |
1110 | except Exception as e: |
|
1108 | except Exception as e: | |
1111 | if self.debug: |
|
1109 | if self.debug: | |
1112 |
print( |
|
1110 | print("Evaluation exception", e) | |
1113 | # trim the expression to remove any invalid prefix |
|
1111 | # trim the expression to remove any invalid prefix | |
1114 | # e.g. user starts `(d[`, so we get `expr = '(d'`, |
|
1112 | # e.g. user starts `(d[`, so we get `expr = '(d'`, | |
1115 | # where parenthesis is not closed. |
|
1113 | # where parenthesis is not closed. | |
@@ -1135,6 +1133,7 b' class DictKeyState(enum.Flag):' | |||||
1135 | - given `d3 = {('a', 'b'): 1}`: `d3['<tab>` will yield `{'a': IN_TUPLE}` as `'a'` can be added. |
|
1133 | - 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}` |
|
1134 | - given `d4 = {'a': 1, ('a', 'b'): 2}`: `d4['<tab>` will yield `{'a': END_OF_ITEM & END_OF_TUPLE}` | |
1137 | """ |
|
1135 | """ | |
|
1136 | ||||
1138 | BASELINE = 0 |
|
1137 | BASELINE = 0 | |
1139 | END_OF_ITEM = enum.auto() |
|
1138 | END_OF_ITEM = enum.auto() | |
1140 | END_OF_TUPLE = enum.auto() |
|
1139 | END_OF_TUPLE = enum.auto() | |
@@ -1179,9 +1178,9 b' def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]:' | |||||
1179 | # we did not match a number |
|
1178 | # we did not match a number | |
1180 | return None |
|
1179 | return None | |
1181 | if token.type == tokenize.OP: |
|
1180 | if token.type == tokenize.OP: | |
1182 |
if token.string == |
|
1181 | if token.string == ",": | |
1183 | break |
|
1182 | break | |
1184 |
if token.string in { |
|
1183 | if token.string in {"+", "-"}: | |
1185 | number = token.string + number |
|
1184 | number = token.string + number | |
1186 | else: |
|
1185 | else: | |
1187 | return None |
|
1186 | return None | |
@@ -1189,9 +1188,9 b' def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]:' | |||||
1189 |
|
1188 | |||
1190 |
|
1189 | |||
1191 | _INT_FORMATS = { |
|
1190 | _INT_FORMATS = { | |
1192 |
|
|
1191 | "0b": bin, | |
1193 |
|
|
1192 | "0o": oct, | |
1194 |
|
|
1193 | "0x": hex, | |
1195 | } |
|
1194 | } | |
1196 |
|
1195 | |||
1197 |
|
1196 | |||
@@ -1199,7 +1198,7 b' def match_dict_keys(' | |||||
1199 | keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], |
|
1198 | keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], | |
1200 | prefix: str, |
|
1199 | prefix: str, | |
1201 | delims: str, |
|
1200 | delims: str, | |
1202 | extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None |
|
1201 | extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None, | |
1203 | ) -> Tuple[str, int, Dict[str, DictKeyState]]: |
|
1202 | ) -> Tuple[str, int, Dict[str, DictKeyState]]: | |
1204 | """Used by dict_key_matches, matching the prefix to a list of keys |
|
1203 | """Used by dict_key_matches, matching the prefix to a list of keys | |
1205 |
|
1204 | |||
@@ -1225,11 +1224,13 b' def match_dict_keys(' | |||||
1225 | """ |
|
1224 | """ | |
1226 | prefix_tuple = extra_prefix if extra_prefix else () |
|
1225 | prefix_tuple = extra_prefix if extra_prefix else () | |
1227 |
|
1226 | |||
1228 |
prefix_tuple_size = sum( |
|
1227 | prefix_tuple_size = sum( | |
1229 | # for pandas, do not count slices as taking space |
|
1228 | [ | |
1230 | not isinstance(k, slice) |
|
1229 | # for pandas, do not count slices as taking space | |
1231 | for k in prefix_tuple |
|
1230 | not isinstance(k, slice) | |
1232 | ]) |
|
1231 | for k in prefix_tuple | |
|
1232 | ] | |||
|
1233 | ) | |||
1233 | text_serializable_types = (str, bytes, int, float, slice) |
|
1234 | text_serializable_types = (str, bytes, int, float, slice) | |
1234 |
|
1235 | |||
1235 | def filter_prefix_tuple(key): |
|
1236 | def filter_prefix_tuple(key): | |
@@ -1247,7 +1248,9 b' def match_dict_keys(' | |||||
1247 | # All checks passed! |
|
1248 | # All checks passed! | |
1248 | return True |
|
1249 | return True | |
1249 |
|
1250 | |||
1250 | filtered_key_is_final: Dict[Union[str, bytes, int, float], DictKeyState] = defaultdict(lambda: DictKeyState.BASELINE) |
|
1251 | filtered_key_is_final: Dict[ | |
|
1252 | Union[str, bytes, int, float], DictKeyState | |||
|
1253 | ] = defaultdict(lambda: DictKeyState.BASELINE) | |||
1251 |
|
1254 | |||
1252 | for k in keys: |
|
1255 | for k in keys: | |
1253 | # If at least one of the matches is not final, mark as undetermined. |
|
1256 | # If at least one of the matches is not final, mark as undetermined. | |
@@ -1259,8 +1262,8 b' def match_dict_keys(' | |||||
1259 | key_fragment = k[prefix_tuple_size] |
|
1262 | key_fragment = k[prefix_tuple_size] | |
1260 | filtered_key_is_final[key_fragment] |= ( |
|
1263 | filtered_key_is_final[key_fragment] |= ( | |
1261 | DictKeyState.END_OF_TUPLE |
|
1264 | DictKeyState.END_OF_TUPLE | |
1262 |
if len(k) == prefix_tuple_size + 1 |
|
1265 | if len(k) == prefix_tuple_size + 1 | |
1263 | DictKeyState.IN_TUPLE |
|
1266 | else DictKeyState.IN_TUPLE | |
1264 | ) |
|
1267 | ) | |
1265 | elif prefix_tuple_size > 0: |
|
1268 | elif prefix_tuple_size > 0: | |
1266 | # we are completing a tuple but this key is not a tuple, |
|
1269 | # we are completing a tuple but this key is not a tuple, | |
@@ -1273,9 +1276,9 b' def match_dict_keys(' | |||||
1273 | filtered_keys = filtered_key_is_final.keys() |
|
1276 | filtered_keys = filtered_key_is_final.keys() | |
1274 |
|
1277 | |||
1275 | if not prefix: |
|
1278 | if not prefix: | |
1276 |
return |
|
1279 | return "", 0, {repr(k): v for k, v in filtered_key_is_final.items()} | |
1277 |
|
1280 | |||
1278 |
quote_match = re.search(' |
|
1281 | quote_match = re.search("(?:\"|')", prefix) | |
1279 | is_user_prefix_numeric = False |
|
1282 | is_user_prefix_numeric = False | |
1280 |
|
1283 | |||
1281 | if quote_match: |
|
1284 | if quote_match: | |
@@ -1284,7 +1287,7 b' def match_dict_keys(' | |||||
1284 | try: |
|
1287 | try: | |
1285 | prefix_str = literal_eval(valid_prefix) |
|
1288 | prefix_str = literal_eval(valid_prefix) | |
1286 | except Exception: |
|
1289 | except Exception: | |
1287 |
return |
|
1290 | return "", 0, {} | |
1288 | else: |
|
1291 | else: | |
1289 | # If it does not look like a string, let's assume |
|
1292 | # If it does not look like a string, let's assume | |
1290 | # we are dealing with a number or variable. |
|
1293 | # we are dealing with a number or variable. | |
@@ -1294,11 +1297,11 b' def match_dict_keys(' | |||||
1294 | if number_match is None: |
|
1297 | if number_match is None: | |
1295 | # The alternative would be to assume that user forgort the quote |
|
1298 | # The alternative would be to assume that user forgort the quote | |
1296 | # and if the substring matches, suggest adding it at the start. |
|
1299 | # and if the substring matches, suggest adding it at the start. | |
1297 |
return |
|
1300 | return "", 0, {} | |
1298 |
|
1301 | |||
1299 | prefix_str = number_match |
|
1302 | prefix_str = number_match | |
1300 | is_user_prefix_numeric = True |
|
1303 | is_user_prefix_numeric = True | |
1301 |
quote = |
|
1304 | quote = "" | |
1302 |
|
1305 | |||
1303 | pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' |
|
1306 | pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' | |
1304 | token_match = re.search(pattern, prefix, re.UNICODE) |
|
1307 | token_match = re.search(pattern, prefix, re.UNICODE) | |
@@ -1333,7 +1336,7 b' def match_dict_keys(' | |||||
1333 | continue |
|
1336 | continue | |
1334 |
|
1337 | |||
1335 | # reformat remainder of key to begin with prefix |
|
1338 | # reformat remainder of key to begin with prefix | |
1336 | rem = str_key[len(prefix_str):] |
|
1339 | rem = str_key[len(prefix_str) :] | |
1337 | # force repr wrapped in ' |
|
1340 | # force repr wrapped in ' | |
1338 | rem_repr = repr(rem + '"') if isinstance(rem, str) else repr(rem + b'"') |
|
1341 | rem_repr = repr(rem + '"') if isinstance(rem, str) else repr(rem + b'"') | |
1339 | rem_repr = rem_repr[1 + rem_repr.index("'"):-2] |
|
1342 | rem_repr = rem_repr[1 + rem_repr.index("'"):-2] | |
@@ -1344,7 +1347,7 b' def match_dict_keys(' | |||||
1344 | rem_repr = rem_repr.replace('"', '\\"') |
|
1347 | rem_repr = rem_repr.replace('"', '\\"') | |
1345 |
|
1348 | |||
1346 | # then reinsert prefix from start of token |
|
1349 | # then reinsert prefix from start of token | |
1347 |
match = |
|
1350 | match = "%s%s" % (token_prefix, rem_repr) | |
1348 |
|
1351 | |||
1349 | matched[match] = filtered_key_is_final[key] |
|
1352 | matched[match] = filtered_key_is_final[key] | |
1350 | return quote, token_start, matched |
|
1353 | return quote, token_start, matched | |
@@ -1572,7 +1575,8 b' def _make_signature(completion)-> str:' | |||||
1572 | _CompleteResult = Dict[str, MatcherResult] |
|
1575 | _CompleteResult = Dict[str, MatcherResult] | |
1573 |
|
1576 | |||
1574 |
|
1577 | |||
1575 |
DICT_MATCHER_REGEX = re.compile( |
|
1578 | DICT_MATCHER_REGEX = re.compile( | |
|
1579 | r"""(?x) | |||
1576 |
|
|
1580 | ( # match dict-referring - or any get item object - expression | |
1577 |
|
|
1581 | .+ | |
1578 |
|
|
1582 | ) | |
@@ -1616,7 +1620,9 b' DICT_MATCHER_REGEX = re.compile(r"""(?x)' | |||||
1616 |
|
|
1620 | ) | |
1617 |
|
|
1621 | )? | |
1618 |
|
|
1622 | $ | |
1619 |
|
|
1623 | """ | |
|
1624 | ) | |||
|
1625 | ||||
1620 |
|
1626 | |||
1621 | def _convert_matcher_v1_result_to_v2( |
|
1627 | def _convert_matcher_v1_result_to_v2( | |
1622 | matches: Sequence[str], |
|
1628 | matches: Sequence[str], | |
@@ -1640,12 +1646,12 b' class IPCompleter(Completer):' | |||||
1640 | @observe('greedy') |
|
1646 | @observe('greedy') | |
1641 | def _greedy_changed(self, change): |
|
1647 | def _greedy_changed(self, change): | |
1642 | """update the splitter and readline delims when greedy is changed""" |
|
1648 | """update the splitter and readline delims when greedy is changed""" | |
1643 |
if change[ |
|
1649 | if change["new"]: | |
1644 |
self.evaluation = |
|
1650 | self.evaluation = "unsafe" | |
1645 | self.auto_close_dict_keys = True |
|
1651 | self.auto_close_dict_keys = True | |
1646 | self.splitter.delims = GREEDY_DELIMS |
|
1652 | self.splitter.delims = GREEDY_DELIMS | |
1647 | else: |
|
1653 | else: | |
1648 |
self.evaluation = |
|
1654 | self.evaluation = "limitted" | |
1649 | self.auto_close_dict_keys = False |
|
1655 | self.auto_close_dict_keys = False | |
1650 | self.splitter.delims = DELIMS |
|
1656 | self.splitter.delims = DELIMS | |
1651 |
|
1657 | |||
@@ -2375,13 +2381,12 b' class IPCompleter(Completer):' | |||||
2375 | return method() |
|
2381 | return method() | |
2376 |
|
2382 | |||
2377 | # Special case some common in-memory dict-like types |
|
2383 | # Special case some common in-memory dict-like types | |
2378 | if (isinstance(obj, dict) or |
|
2384 | if isinstance(obj, dict) or _safe_isinstance(obj, "pandas", "DataFrame"): | |
2379 | _safe_isinstance(obj, 'pandas', 'DataFrame')): |
|
|||
2380 | try: |
|
2385 | try: | |
2381 | return list(obj.keys()) |
|
2386 | return list(obj.keys()) | |
2382 | except Exception: |
|
2387 | except Exception: | |
2383 | return [] |
|
2388 | return [] | |
2384 |
elif _safe_isinstance(obj, |
|
2389 | elif _safe_isinstance(obj, "pandas", "core", "indexing", "_LocIndexer"): | |
2385 | try: |
|
2390 | try: | |
2386 | return list(obj.obj.keys()) |
|
2391 | return list(obj.obj.keys()) | |
2387 | except Exception: |
|
2392 | except Exception: | |
@@ -2408,7 +2413,7 b' class IPCompleter(Completer):' | |||||
2408 |
|
2413 | |||
2409 | # Short-circuit on closed dictionary (regular expression would |
|
2414 | # Short-circuit on closed dictionary (regular expression would | |
2410 | # not match anyway, but would take quite a while). |
|
2415 | # not match anyway, but would take quite a while). | |
2411 |
if self.text_until_cursor.strip().endswith( |
|
2416 | if self.text_until_cursor.strip().endswith("]"): | |
2412 | return [] |
|
2417 | return [] | |
2413 |
|
2418 | |||
2414 | match = DICT_MATCHER_REGEX.search(self.text_until_cursor) |
|
2419 | match = DICT_MATCHER_REGEX.search(self.text_until_cursor) | |
@@ -2433,15 +2438,12 b' class IPCompleter(Completer):' | |||||
2433 | globals_=self.global_namespace, |
|
2438 | globals_=self.global_namespace, | |
2434 | locals_=self.namespace, |
|
2439 | locals_=self.namespace, | |
2435 | evaluation=self.evaluation, |
|
2440 | evaluation=self.evaluation, | |
2436 | in_subscript=True |
|
2441 | in_subscript=True, | |
2437 | ) |
|
2442 | ), | |
2438 | ) |
|
2443 | ) | |
2439 |
|
2444 | |||
2440 | closing_quote, token_offset, matches = match_dict_keys( |
|
2445 | closing_quote, token_offset, matches = match_dict_keys( | |
2441 | keys, |
|
2446 | keys, key_prefix, self.splitter.delims, extra_prefix=tuple_prefix | |
2442 | key_prefix, |
|
|||
2443 | self.splitter.delims, |
|
|||
2444 | extra_prefix=tuple_prefix |
|
|||
2445 | ) |
|
2447 | ) | |
2446 | if not matches: |
|
2448 | if not matches: | |
2447 | return [] |
|
2449 | return [] | |
@@ -2469,11 +2471,11 b' class IPCompleter(Completer):' | |||||
2469 | can_close_quote = False |
|
2471 | can_close_quote = False | |
2470 | can_close_bracket = False |
|
2472 | can_close_bracket = False | |
2471 |
|
2473 | |||
2472 | continuation = self.line_buffer[len(self.text_until_cursor):].strip() |
|
2474 | continuation = self.line_buffer[len(self.text_until_cursor) :].strip() | |
2473 |
|
2475 | |||
2474 | if continuation.startswith(closing_quote): |
|
2476 | if continuation.startswith(closing_quote): | |
2475 | # do not close if already closed, e.g. `d['a<tab>'` |
|
2477 | # do not close if already closed, e.g. `d['a<tab>'` | |
2476 | continuation = continuation[len(closing_quote):] |
|
2478 | continuation = continuation[len(closing_quote) :] | |
2477 | else: |
|
2479 | else: | |
2478 | can_close_quote = True |
|
2480 | can_close_quote = True | |
2479 |
|
2481 | |||
@@ -2483,8 +2485,14 b' class IPCompleter(Completer):' | |||||
2483 | # handling it is out of scope, so let's avoid appending suffixes. |
|
2485 | # handling it is out of scope, so let's avoid appending suffixes. | |
2484 | has_known_tuple_handling = isinstance(obj, dict) |
|
2486 | has_known_tuple_handling = isinstance(obj, dict) | |
2485 |
|
2487 | |||
2486 | can_close_bracket = not continuation.startswith(']') and self.auto_close_dict_keys |
|
2488 | can_close_bracket = ( | |
2487 |
|
|
2489 | not continuation.startswith("]") and self.auto_close_dict_keys | |
|
2490 | ) | |||
|
2491 | can_close_tuple_item = ( | |||
|
2492 | not continuation.startswith(",") | |||
|
2493 | and has_known_tuple_handling | |||
|
2494 | and self.auto_close_dict_keys | |||
|
2495 | ) | |||
2488 | can_close_quote = can_close_quote and self.auto_close_dict_keys |
|
2496 | can_close_quote = can_close_quote and self.auto_close_dict_keys | |
2489 |
|
2497 | |||
2490 | # fast path if closing qoute should be appended but not suffix is allowed |
|
2498 | # fast path if closing qoute should be appended but not suffix is allowed | |
@@ -2507,9 +2515,9 b' class IPCompleter(Completer):' | |||||
2507 | pass |
|
2515 | pass | |
2508 |
|
2516 | |||
2509 | if state_flag in end_of_tuple_or_item and can_close_bracket: |
|
2517 | if state_flag in end_of_tuple_or_item and can_close_bracket: | |
2510 |
result += |
|
2518 | result += "]" | |
2511 | if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: |
|
2519 | if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: | |
2512 |
result += |
|
2520 | result += ", " | |
2513 | results.append(result) |
|
2521 | results.append(result) | |
2514 | return results |
|
2522 | return results | |
2515 |
|
2523 |
@@ -16,20 +16,24 b' else:' | |||||
16 |
|
16 | |||
17 |
|
17 | |||
18 | class HasGetItem(Protocol): |
|
18 | class HasGetItem(Protocol): | |
19 |
def __getitem__(self, key) -> None: |
|
19 | def __getitem__(self, key) -> None: | |
|
20 | ... | |||
20 |
|
21 | |||
21 |
|
22 | |||
22 | class InstancesHaveGetItem(Protocol): |
|
23 | class InstancesHaveGetItem(Protocol): | |
23 |
def __call__(self) -> HasGetItem: |
|
24 | def __call__(self) -> HasGetItem: | |
|
25 | ... | |||
24 |
|
26 | |||
25 |
|
27 | |||
26 | class HasGetAttr(Protocol): |
|
28 | class HasGetAttr(Protocol): | |
27 |
def __getattr__(self, key) -> None: |
|
29 | def __getattr__(self, key) -> None: | |
|
30 | ... | |||
28 |
|
31 | |||
29 |
|
32 | |||
30 | class DoesNotHaveGetAttr(Protocol): |
|
33 | class DoesNotHaveGetAttr(Protocol): | |
31 | pass |
|
34 | pass | |
32 |
|
35 | |||
|
36 | ||||
33 | # By default `__getattr__` is not explicitly implemented on most objects |
|
37 | # By default `__getattr__` is not explicitly implemented on most objects | |
34 | MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] |
|
38 | MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] | |
35 |
|
39 | |||
@@ -38,22 +42,16 b' def unbind_method(func: Callable) -> Union[Callable, None]:' | |||||
38 | """Get unbound method for given bound method. |
|
42 | """Get unbound method for given bound method. | |
39 |
|
43 | |||
40 | Returns None if cannot get unbound method.""" |
|
44 | Returns None if cannot get unbound method.""" | |
41 |
owner = getattr(func, |
|
45 | owner = getattr(func, "__self__", None) | |
42 | owner_class = type(owner) |
|
46 | owner_class = type(owner) | |
43 |
name = getattr(func, |
|
47 | name = getattr(func, "__name__", None) | |
44 |
instance_dict_overrides = getattr(owner, |
|
48 | instance_dict_overrides = getattr(owner, "__dict__", None) | |
45 | if ( |
|
49 | if ( | |
46 | owner is not None |
|
50 | owner is not None | |
47 | and |
|
51 | and name | |
48 |
|
|
52 | and ( | |
49 | and |
|
|||
50 | ( |
|
|||
51 | not instance_dict_overrides |
|
53 | not instance_dict_overrides | |
52 | or |
|
54 | or (instance_dict_overrides and name not in instance_dict_overrides) | |
53 | ( |
|
|||
54 | instance_dict_overrides |
|
|||
55 | and name not in instance_dict_overrides |
|
|||
56 | ) |
|
|||
57 | ) |
|
55 | ) | |
58 | ): |
|
56 | ): | |
59 | return getattr(owner_class, name) |
|
57 | return getattr(owner_class, name) | |
@@ -86,7 +84,13 b' class EvaluationPolicy:' | |||||
86 | if owner_method and owner_method in self.allowed_calls: |
|
84 | if owner_method and owner_method in self.allowed_calls: | |
87 | return True |
|
85 | return True | |
88 |
|
86 | |||
89 | def has_original_dunder_external(value, module_name, access_path, method_name,): |
|
87 | ||
|
88 | def has_original_dunder_external( | |||
|
89 | value, | |||
|
90 | module_name, | |||
|
91 | access_path, | |||
|
92 | method_name, | |||
|
93 | ): | |||
90 | try: |
|
94 | try: | |
91 | if module_name not in sys.modules: |
|
95 | if module_name not in sys.modules: | |
92 | return False |
|
96 | return False | |
@@ -106,11 +110,7 b' def has_original_dunder_external(value, module_name, access_path, method_name,):' | |||||
106 |
|
110 | |||
107 |
|
111 | |||
108 | def has_original_dunder( |
|
112 | def has_original_dunder( | |
109 | value, |
|
113 | value, allowed_types, allowed_methods, allowed_external, method_name | |
110 | allowed_types, |
|
|||
111 | allowed_methods, |
|
|||
112 | allowed_external, |
|
|||
113 | method_name |
|
|||
114 | ): |
|
114 | ): | |
115 | # note: Python ignores `__getattr__`/`__getitem__` on instances, |
|
115 | # note: Python ignores `__getattr__`/`__getitem__` on instances, | |
116 | # we only need to check at class level |
|
116 | # we only need to check at class level | |
@@ -148,14 +148,14 b' class SelectivePolicy(EvaluationPolicy):' | |||||
148 | allowed_types=self.allowed_getattr, |
|
148 | allowed_types=self.allowed_getattr, | |
149 | allowed_methods=self._getattribute_methods, |
|
149 | allowed_methods=self._getattribute_methods, | |
150 | allowed_external=self.allowed_getattr_external, |
|
150 | allowed_external=self.allowed_getattr_external, | |
151 |
method_name= |
|
151 | method_name="__getattribute__", | |
152 | ) |
|
152 | ) | |
153 | has_original_attr = has_original_dunder( |
|
153 | has_original_attr = has_original_dunder( | |
154 | value, |
|
154 | value, | |
155 | allowed_types=self.allowed_getattr, |
|
155 | allowed_types=self.allowed_getattr, | |
156 | allowed_methods=self._getattr_methods, |
|
156 | allowed_methods=self._getattr_methods, | |
157 | allowed_external=self.allowed_getattr_external, |
|
157 | allowed_external=self.allowed_getattr_external, | |
158 |
method_name= |
|
158 | method_name="__getattr__", | |
159 | ) |
|
159 | ) | |
160 | # Many objects do not have `__getattr__`, this is fine |
|
160 | # Many objects do not have `__getattr__`, this is fine | |
161 | if has_original_attr is None and has_original_attribute: |
|
161 | if has_original_attr is None and has_original_attribute: | |
@@ -168,7 +168,6 b' class SelectivePolicy(EvaluationPolicy):' | |||||
168 | if self.can_get_attr(value, attr): |
|
168 | if self.can_get_attr(value, attr): | |
169 | return getattr(value, attr) |
|
169 | return getattr(value, attr) | |
170 |
|
170 | |||
171 |
|
||||
172 | def can_get_item(self, value, item): |
|
171 | def can_get_item(self, value, item): | |
173 | """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" |
|
172 | """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" | |
174 | return has_original_dunder( |
|
173 | return has_original_dunder( | |
@@ -176,29 +175,20 b' class SelectivePolicy(EvaluationPolicy):' | |||||
176 | allowed_types=self.allowed_getitem, |
|
175 | allowed_types=self.allowed_getitem, | |
177 | allowed_methods=self._getitem_methods, |
|
176 | allowed_methods=self._getitem_methods, | |
178 | allowed_external=self.allowed_getitem_external, |
|
177 | allowed_external=self.allowed_getitem_external, | |
179 |
method_name= |
|
178 | method_name="__getitem__", | |
180 | ) |
|
179 | ) | |
181 |
|
180 | |||
182 | @cached_property |
|
181 | @cached_property | |
183 | def _getitem_methods(self) -> Set[Callable]: |
|
182 | def _getitem_methods(self) -> Set[Callable]: | |
184 | return self._safe_get_methods( |
|
183 | return self._safe_get_methods(self.allowed_getitem, "__getitem__") | |
185 | self.allowed_getitem, |
|
|||
186 | '__getitem__' |
|
|||
187 | ) |
|
|||
188 |
|
184 | |||
189 | @cached_property |
|
185 | @cached_property | |
190 | def _getattr_methods(self) -> Set[Callable]: |
|
186 | def _getattr_methods(self) -> Set[Callable]: | |
191 | return self._safe_get_methods( |
|
187 | return self._safe_get_methods(self.allowed_getattr, "__getattr__") | |
192 | self.allowed_getattr, |
|
|||
193 | '__getattr__' |
|
|||
194 | ) |
|
|||
195 |
|
188 | |||
196 | @cached_property |
|
189 | @cached_property | |
197 | def _getattribute_methods(self) -> Set[Callable]: |
|
190 | def _getattribute_methods(self) -> Set[Callable]: | |
198 | return self._safe_get_methods( |
|
191 | return self._safe_get_methods(self.allowed_getattr, "__getattribute__") | |
199 | self.allowed_getattr, |
|
|||
200 | '__getattribute__' |
|
|||
201 | ) |
|
|||
202 |
|
192 | |||
203 | def _safe_get_methods(self, classes, name) -> Set[Callable]: |
|
193 | def _safe_get_methods(self, classes, name) -> Set[Callable]: | |
204 | return { |
|
194 | return { | |
@@ -216,7 +206,9 b' class DummyNamedTuple(NamedTuple):' | |||||
216 | class EvaluationContext(NamedTuple): |
|
206 | class EvaluationContext(NamedTuple): | |
217 | locals_: dict |
|
207 | locals_: dict | |
218 | globals_: dict |
|
208 | globals_: dict | |
219 | evaluation: Literal['forbidden', 'minimal', 'limitted', 'unsafe', 'dangerous'] = 'forbidden' |
|
209 | evaluation: Literal[ | |
|
210 | "forbidden", "minimal", "limitted", "unsafe", "dangerous" | |||
|
211 | ] = "forbidden" | |||
220 | in_subscript: bool = False |
|
212 | in_subscript: bool = False | |
221 |
|
213 | |||
222 |
|
214 | |||
@@ -224,21 +216,20 b' class IdentitySubscript:' | |||||
224 | def __getitem__(self, key): |
|
216 | def __getitem__(self, key): | |
225 | return key |
|
217 | return key | |
226 |
|
218 | |||
|
219 | ||||
227 | IDENTITY_SUBSCRIPT = IdentitySubscript() |
|
220 | IDENTITY_SUBSCRIPT = IdentitySubscript() | |
228 |
SUBSCRIPT_MARKER = |
|
221 | SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" | |
|
222 | ||||
229 |
|
223 | |||
230 | class GuardRejection(ValueError): |
|
224 | class GuardRejection(ValueError): | |
231 | pass |
|
225 | pass | |
232 |
|
226 | |||
233 |
|
227 | |||
234 | def guarded_eval( |
|
228 | def guarded_eval(code: str, context: EvaluationContext): | |
235 | code: str, |
|
|||
236 | context: EvaluationContext |
|
|||
237 | ): |
|
|||
238 | locals_ = context.locals_ |
|
229 | locals_ = context.locals_ | |
239 |
|
230 | |||
240 |
if context.evaluation == |
|
231 | if context.evaluation == "forbidden": | |
241 |
raise GuardRejection( |
|
232 | raise GuardRejection("Forbidden mode") | |
242 |
|
233 | |||
243 | # note: not using `ast.literal_eval` as it does not implement |
|
234 | # note: not using `ast.literal_eval` as it does not implement | |
244 | # getitem at all, for example it fails on simple `[0][1]` |
|
235 | # getitem at all, for example it fails on simple `[0][1]` | |
@@ -252,19 +243,17 b' def guarded_eval(' | |||||
252 | return tuple() |
|
243 | return tuple() | |
253 | locals_ = locals_.copy() |
|
244 | locals_ = locals_.copy() | |
254 | locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT |
|
245 | locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT | |
255 |
code = SUBSCRIPT_MARKER + |
|
246 | code = SUBSCRIPT_MARKER + "[" + code + "]" | |
256 | context = EvaluationContext(**{ |
|
247 | context = EvaluationContext(**{**context._asdict(), **{"locals_": locals_}}) | |
257 | **context._asdict(), |
|
|||
258 | **{'locals_': locals_} |
|
|||
259 | }) |
|
|||
260 |
|
248 | |||
261 |
if context.evaluation == |
|
249 | if context.evaluation == "dangerous": | |
262 | return eval(code, context.globals_, context.locals_) |
|
250 | return eval(code, context.globals_, context.locals_) | |
263 |
|
251 | |||
264 |
expression = ast.parse(code, mode= |
|
252 | expression = ast.parse(code, mode="eval") | |
265 |
|
253 | |||
266 | return eval_node(expression, context) |
|
254 | return eval_node(expression, context) | |
267 |
|
255 | |||
|
256 | ||||
268 | def eval_node(node: Union[ast.AST, None], context: EvaluationContext): |
|
257 | def eval_node(node: Union[ast.AST, None], context: EvaluationContext): | |
269 | """ |
|
258 | """ | |
270 | Evaluate AST node in provided context. |
|
259 | Evaluate AST node in provided context. | |
@@ -314,7 +303,7 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
314 | if isinstance(node.op, ast.Mod): |
|
303 | if isinstance(node.op, ast.Mod): | |
315 | return left % right |
|
304 | return left % right | |
316 | if isinstance(node.op, ast.Pow): |
|
305 | if isinstance(node.op, ast.Pow): | |
317 |
return left |
|
306 | return left**right | |
318 | if isinstance(node.op, ast.LShift): |
|
307 | if isinstance(node.op, ast.LShift): | |
319 | return left << right |
|
308 | return left << right | |
320 | if isinstance(node.op, ast.RShift): |
|
309 | if isinstance(node.op, ast.RShift): | |
@@ -332,36 +321,26 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
332 | if isinstance(node, ast.Index): |
|
321 | if isinstance(node, ast.Index): | |
333 | return eval_node(node.value, context) |
|
322 | return eval_node(node.value, context) | |
334 | if isinstance(node, ast.Tuple): |
|
323 | if isinstance(node, ast.Tuple): | |
335 | return tuple( |
|
324 | return tuple(eval_node(e, context) for e in node.elts) | |
336 | eval_node(e, context) |
|
|||
337 | for e in node.elts |
|
|||
338 | ) |
|
|||
339 | if isinstance(node, ast.List): |
|
325 | if isinstance(node, ast.List): | |
340 | return [ |
|
326 | return [eval_node(e, context) for e in node.elts] | |
341 | eval_node(e, context) |
|
|||
342 | for e in node.elts |
|
|||
343 | ] |
|
|||
344 | if isinstance(node, ast.Set): |
|
327 | if isinstance(node, ast.Set): | |
345 | return { |
|
328 | return {eval_node(e, context) for e in node.elts} | |
346 | eval_node(e, context) |
|
|||
347 | for e in node.elts |
|
|||
348 | } |
|
|||
349 | if isinstance(node, ast.Dict): |
|
329 | if isinstance(node, ast.Dict): | |
350 |
return dict( |
|
330 | return dict( | |
351 | [eval_node(k, context) for k in node.keys], |
|
331 | zip( | |
352 |
[eval_node( |
|
332 | [eval_node(k, context) for k in node.keys], | |
353 | )) |
|
333 | [eval_node(v, context) for v in node.values], | |
|
334 | ) | |||
|
335 | ) | |||
354 | if isinstance(node, ast.Slice): |
|
336 | if isinstance(node, ast.Slice): | |
355 | return slice( |
|
337 | return slice( | |
356 | eval_node(node.lower, context), |
|
338 | eval_node(node.lower, context), | |
357 | eval_node(node.upper, context), |
|
339 | eval_node(node.upper, context), | |
358 | eval_node(node.step, context) |
|
340 | eval_node(node.step, context), | |
359 | ) |
|
341 | ) | |
360 | if isinstance(node, ast.ExtSlice): |
|
342 | if isinstance(node, ast.ExtSlice): | |
361 | return tuple([ |
|
343 | return tuple([eval_node(dim, context) for dim in node.dims]) | |
362 | eval_node(dim, context) |
|
|||
363 | for dim in node.dims |
|
|||
364 | ]) |
|
|||
365 | if isinstance(node, ast.UnaryOp): |
|
344 | if isinstance(node, ast.UnaryOp): | |
366 | # TODO: add guards |
|
345 | # TODO: add guards | |
367 | value = eval_node(node.operand, context) |
|
346 | value = eval_node(node.operand, context) | |
@@ -373,16 +352,16 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
373 | return ~value |
|
352 | return ~value | |
374 | if isinstance(node.op, ast.Not): |
|
353 | if isinstance(node.op, ast.Not): | |
375 | return not value |
|
354 | return not value | |
376 |
raise ValueError( |
|
355 | raise ValueError("Unhandled unary operation:", node.op) | |
377 | if isinstance(node, ast.Subscript): |
|
356 | if isinstance(node, ast.Subscript): | |
378 | value = eval_node(node.value, context) |
|
357 | value = eval_node(node.value, context) | |
379 | slice_ = eval_node(node.slice, context) |
|
358 | slice_ = eval_node(node.slice, context) | |
380 | if policy.can_get_item(value, slice_): |
|
359 | if policy.can_get_item(value, slice_): | |
381 | return value[slice_] |
|
360 | return value[slice_] | |
382 | raise GuardRejection( |
|
361 | raise GuardRejection( | |
383 |
|
|
362 | "Subscript access (`__getitem__`) for", | |
384 | type(value), # not joined to avoid calling `repr` |
|
363 | type(value), # not joined to avoid calling `repr` | |
385 |
f |
|
364 | f" not allowed in {context.evaluation} mode", | |
386 | ) |
|
365 | ) | |
387 | if isinstance(node, ast.Name): |
|
366 | if isinstance(node, ast.Name): | |
388 | if policy.allow_locals_access and node.id in context.locals_: |
|
367 | if policy.allow_locals_access and node.id in context.locals_: | |
@@ -393,49 +372,46 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
393 | return __builtins__[node.id] |
|
372 | return __builtins__[node.id] | |
394 | if not policy.allow_globals_access and not policy.allow_locals_access: |
|
373 | if not policy.allow_globals_access and not policy.allow_locals_access: | |
395 | raise GuardRejection( |
|
374 | raise GuardRejection( | |
396 |
f |
|
375 | f"Namespace access not allowed in {context.evaluation} mode" | |
397 | ) |
|
376 | ) | |
398 | else: |
|
377 | else: | |
399 |
raise NameError(f |
|
378 | raise NameError(f"{node.id} not found in locals nor globals") | |
400 | if isinstance(node, ast.Attribute): |
|
379 | if isinstance(node, ast.Attribute): | |
401 | value = eval_node(node.value, context) |
|
380 | value = eval_node(node.value, context) | |
402 | if policy.can_get_attr(value, node.attr): |
|
381 | if policy.can_get_attr(value, node.attr): | |
403 | return getattr(value, node.attr) |
|
382 | return getattr(value, node.attr) | |
404 | raise GuardRejection( |
|
383 | raise GuardRejection( | |
405 |
|
|
384 | "Attribute access (`__getattr__`) for", | |
406 | type(value), # not joined to avoid calling `repr` |
|
385 | type(value), # not joined to avoid calling `repr` | |
407 |
f |
|
386 | f"not allowed in {context.evaluation} mode", | |
408 | ) |
|
387 | ) | |
409 | if isinstance(node, ast.IfExp): |
|
388 | if isinstance(node, ast.IfExp): | |
410 | test = eval_node(node.test, context) |
|
389 | test = eval_node(node.test, context) | |
411 | if test: |
|
390 | if test: | |
412 |
|
|
391 | return eval_node(node.body, context) | |
413 | else: |
|
392 | else: | |
414 | return eval_node(node.orelse, context) |
|
393 | return eval_node(node.orelse, context) | |
415 | if isinstance(node, ast.Call): |
|
394 | if isinstance(node, ast.Call): | |
416 | func = eval_node(node.func, context) |
|
395 | func = eval_node(node.func, context) | |
417 | print(node.keywords) |
|
396 | print(node.keywords) | |
418 | if policy.can_call(func) and not node.keywords: |
|
397 | if policy.can_call(func) and not node.keywords: | |
419 | args = [ |
|
398 | args = [eval_node(arg, context) for arg in node.args] | |
420 | eval_node(arg, context) |
|
|||
421 | for arg in node.args |
|
|||
422 | ] |
|
|||
423 | return func(*args) |
|
399 | return func(*args) | |
424 | raise GuardRejection( |
|
400 | raise GuardRejection( | |
425 |
|
|
401 | "Call for", | |
426 | func, # not joined to avoid calling `repr` |
|
402 | func, # not joined to avoid calling `repr` | |
427 |
f |
|
403 | f"not allowed in {context.evaluation} mode", | |
428 | ) |
|
404 | ) | |
429 |
raise ValueError( |
|
405 | raise ValueError("Unhandled node", node) | |
430 |
|
406 | |||
431 |
|
407 | |||
432 | SUPPORTED_EXTERNAL_GETITEM = { |
|
408 | SUPPORTED_EXTERNAL_GETITEM = { | |
433 |
( |
|
409 | ("pandas", "core", "indexing", "_iLocIndexer"), | |
434 |
( |
|
410 | ("pandas", "core", "indexing", "_LocIndexer"), | |
435 |
( |
|
411 | ("pandas", "DataFrame"), | |
436 |
( |
|
412 | ("pandas", "Series"), | |
437 |
( |
|
413 | ("numpy", "ndarray"), | |
438 |
( |
|
414 | ("numpy", "void"), | |
439 | } |
|
415 | } | |
440 |
|
416 | |||
441 | BUILTIN_GETITEM = { |
|
417 | BUILTIN_GETITEM = { | |
@@ -452,20 +428,17 b' BUILTIN_GETITEM = {' | |||||
452 | collections.UserList, |
|
428 | collections.UserList, | |
453 | collections.UserString, |
|
429 | collections.UserString, | |
454 | DummyNamedTuple, |
|
430 | DummyNamedTuple, | |
455 | IdentitySubscript |
|
431 | IdentitySubscript, | |
456 | } |
|
432 | } | |
457 |
|
433 | |||
458 |
|
434 | |||
459 | def _list_methods(cls, source=None): |
|
435 | def _list_methods(cls, source=None): | |
460 | """For use on immutable objects or with methods returning a copy""" |
|
436 | """For use on immutable objects or with methods returning a copy""" | |
461 | return [ |
|
437 | return [getattr(cls, k) for k in (source if source else dir(cls))] | |
462 | getattr(cls, k) |
|
|||
463 | for k in (source if source else dir(cls)) |
|
|||
464 | ] |
|
|||
465 |
|
438 | |||
466 |
|
439 | |||
467 |
dict_non_mutating_methods = ( |
|
440 | dict_non_mutating_methods = ("copy", "keys", "values", "items") | |
468 |
list_non_mutating_methods = ( |
|
441 | list_non_mutating_methods = ("copy", "index", "count") | |
469 | set_non_mutating_methods = set(dir(set)) & set(dir(frozenset)) |
|
442 | set_non_mutating_methods = set(dir(set)) & set(dir(frozenset)) | |
470 |
|
443 | |||
471 |
|
444 | |||
@@ -504,20 +477,20 b' ALLOWED_CALLS = {' | |||||
504 | collections.Counter, |
|
477 | collections.Counter, | |
505 | *_list_methods(collections.Counter, dict_non_mutating_methods), |
|
478 | *_list_methods(collections.Counter, dict_non_mutating_methods), | |
506 | collections.Counter.elements, |
|
479 | collections.Counter.elements, | |
507 | collections.Counter.most_common |
|
480 | collections.Counter.most_common, | |
508 | } |
|
481 | } | |
509 |
|
482 | |||
510 | EVALUATION_POLICIES = { |
|
483 | EVALUATION_POLICIES = { | |
511 |
|
|
484 | "minimal": EvaluationPolicy( | |
512 | allow_builtins_access=True, |
|
485 | allow_builtins_access=True, | |
513 | allow_locals_access=False, |
|
486 | allow_locals_access=False, | |
514 | allow_globals_access=False, |
|
487 | allow_globals_access=False, | |
515 | allow_item_access=False, |
|
488 | allow_item_access=False, | |
516 | allow_attr_access=False, |
|
489 | allow_attr_access=False, | |
517 | allowed_calls=set(), |
|
490 | allowed_calls=set(), | |
518 | allow_any_calls=False |
|
491 | allow_any_calls=False, | |
519 | ), |
|
492 | ), | |
520 |
|
|
493 | "limitted": SelectivePolicy( | |
521 | # TODO: |
|
494 | # TODO: | |
522 | # - should reject binary and unary operations if custom methods would be dispatched |
|
495 | # - should reject binary and unary operations if custom methods would be dispatched | |
523 | allowed_getitem=BUILTIN_GETITEM, |
|
496 | allowed_getitem=BUILTIN_GETITEM, | |
@@ -529,24 +502,24 b' EVALUATION_POLICIES = {' | |||||
529 | object, |
|
502 | object, | |
530 | type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. |
|
503 | type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. | |
531 | dict_keys, |
|
504 | dict_keys, | |
532 | method_descriptor |
|
505 | method_descriptor, | |
533 | }, |
|
506 | }, | |
534 | allowed_getattr_external={ |
|
507 | allowed_getattr_external={ | |
535 | # pandas Series/Frame implements custom `__getattr__` |
|
508 | # pandas Series/Frame implements custom `__getattr__` | |
536 |
( |
|
509 | ("pandas", "DataFrame"), | |
537 |
( |
|
510 | ("pandas", "Series"), | |
538 | }, |
|
511 | }, | |
539 | allow_builtins_access=True, |
|
512 | allow_builtins_access=True, | |
540 | allow_locals_access=True, |
|
513 | allow_locals_access=True, | |
541 | allow_globals_access=True, |
|
514 | allow_globals_access=True, | |
542 | allowed_calls=ALLOWED_CALLS |
|
515 | allowed_calls=ALLOWED_CALLS, | |
543 | ), |
|
516 | ), | |
544 |
|
|
517 | "unsafe": EvaluationPolicy( | |
545 | allow_builtins_access=True, |
|
518 | allow_builtins_access=True, | |
546 | allow_locals_access=True, |
|
519 | allow_locals_access=True, | |
547 | allow_globals_access=True, |
|
520 | allow_globals_access=True, | |
548 | allow_attr_access=True, |
|
521 | allow_attr_access=True, | |
549 | allow_item_access=True, |
|
522 | allow_item_access=True, | |
550 | allow_any_calls=True |
|
523 | allow_any_calls=True, | |
551 | ) |
|
524 | ), | |
552 | } No newline at end of file |
|
525 | } |
@@ -868,21 +868,16 b' class TestCompleter(unittest.TestCase):' | |||||
868 | assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) |
|
868 | assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) | |
869 |
|
869 | |||
870 | # Completion on first item of tuple |
|
870 | # Completion on first item of tuple | |
871 |
keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, |
|
871 | keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, "test")] | |
872 | assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) |
|
872 | assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) | |
873 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) |
|
873 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) | |
874 |
|
874 | |||
875 | # Completion on numbers |
|
875 | # Completion on numbers | |
876 | keys = [ |
|
876 | keys = [0xDEADBEEF, 1111, 1234, "1999", 0b10101, 22] # 3735928559 # 21 | |
877 | 0xdeadbeef, # 3735928559 |
|
|||
878 | 1111, 1234, "1999", |
|
|||
879 | 0b10101, # 21 |
|
|||
880 | 22 |
|
|||
881 | ] |
|
|||
882 | assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) |
|
877 | assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) | |
883 | assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) |
|
878 | assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) | |
884 | assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) |
|
879 | assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) | |
885 |
assert match(keys, "0b101", delims=delims) == ("", 0, [ |
|
880 | assert match(keys, "0b101", delims=delims) == ("", 0, ["0b10101", "0b10110"]) | |
886 |
|
881 | |||
887 | def test_match_dict_keys_tuple(self): |
|
882 | def test_match_dict_keys_tuple(self): | |
888 | """ |
|
883 | """ | |
@@ -898,30 +893,90 b' class TestCompleter(unittest.TestCase):' | |||||
898 | return quote, offset, list(matches) |
|
893 | return quote, offset, list(matches) | |
899 |
|
894 | |||
900 | # Completion on first key == "foo" |
|
895 | # Completion on first key == "foo" | |
901 |
assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( |
|
896 | assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( | |
902 | assert match(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) |
|
897 | "'", | |
903 | assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) |
|
898 | 1, | |
904 | assert match(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) |
|
899 | ["bar", "oof"], | |
905 | assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) |
|
900 | ) | |
906 |
assert match(keys, " |
|
901 | assert match(keys, '"', delims=delims, extra_prefix=("foo",)) == ( | |
907 | assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) |
|
902 | '"', | |
908 | assert match(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) |
|
903 | 1, | |
|
904 | ["bar", "oof"], | |||
|
905 | ) | |||
|
906 | assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ( | |||
|
907 | "'", | |||
|
908 | 1, | |||
|
909 | ["oof"], | |||
|
910 | ) | |||
|
911 | assert match(keys, '"o', delims=delims, extra_prefix=("foo",)) == ( | |||
|
912 | '"', | |||
|
913 | 1, | |||
|
914 | ["oof"], | |||
|
915 | ) | |||
|
916 | assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ( | |||
|
917 | "'", | |||
|
918 | 2, | |||
|
919 | ["bar"], | |||
|
920 | ) | |||
|
921 | assert match(keys, 'b"', delims=delims, extra_prefix=("foo",)) == ( | |||
|
922 | '"', | |||
|
923 | 2, | |||
|
924 | ["bar"], | |||
|
925 | ) | |||
|
926 | assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ( | |||
|
927 | "'", | |||
|
928 | 2, | |||
|
929 | ["bar"], | |||
|
930 | ) | |||
|
931 | assert match(keys, 'b"b', delims=delims, extra_prefix=("foo",)) == ( | |||
|
932 | '"', | |||
|
933 | 2, | |||
|
934 | ["bar"], | |||
|
935 | ) | |||
909 |
|
936 | |||
910 | # No Completion |
|
937 | # No Completion | |
911 | assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) |
|
938 | assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) | |
912 | assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) |
|
939 | assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) | |
913 |
|
940 | |||
914 |
keys = [( |
|
941 | keys = [("foo1", "foo2", "foo3", "foo4"), ("foo1", "foo2", "bar", "foo4")] | |
915 |
assert match(keys, "'foo", delims=delims, extra_prefix=( |
|
942 | assert match(keys, "'foo", delims=delims, extra_prefix=("foo1",)) == ( | |
916 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) |
|
943 | "'", | |
917 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) |
|
944 | 1, | |
918 | assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) |
|
945 | ["foo2"], | |
|
946 | ) | |||
|
947 | assert match(keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2")) == ( | |||
|
948 | "'", | |||
|
949 | 1, | |||
|
950 | ["foo3"], | |||
|
951 | ) | |||
|
952 | assert match( | |||
|
953 | keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3") | |||
|
954 | ) == ("'", 1, ["foo4"]) | |||
|
955 | assert match( | |||
|
956 | keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3", "foo4") | |||
|
957 | ) == ("'", 1, []) | |||
919 |
|
958 | |||
920 | keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] |
|
959 | keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] | |
921 |
assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( |
|
960 | assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( | |
922 | assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ("", 0, ["1111", "'2222'"]) |
|
961 | "'", | |
923 | assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar"]) |
|
962 | 1, | |
924 | assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ("", 0, ["'bar'", "4444"]) |
|
963 | ["2222"], | |
|
964 | ) | |||
|
965 | assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ( | |||
|
966 | "", | |||
|
967 | 0, | |||
|
968 | ["1111", "'2222'"], | |||
|
969 | ) | |||
|
970 | assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ( | |||
|
971 | "'", | |||
|
972 | 1, | |||
|
973 | ["bar"], | |||
|
974 | ) | |||
|
975 | assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ( | |||
|
976 | "", | |||
|
977 | 0, | |||
|
978 | ["'bar'", "4444"], | |||
|
979 | ) | |||
925 | assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) |
|
980 | assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) | |
926 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) |
|
981 | assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) | |
927 |
|
982 | |||
@@ -932,16 +987,16 b' class TestCompleter(unittest.TestCase):' | |||||
932 |
|
987 | |||
933 | ip.user_ns["d"] = { |
|
988 | ip.user_ns["d"] = { | |
934 | # tuple only |
|
989 | # tuple only | |
935 |
( |
|
990 | ("aa", 11): None, | |
936 | # tuple and non-tuple |
|
991 | # tuple and non-tuple | |
937 |
( |
|
992 | ("bb", 22): None, | |
938 |
|
|
993 | "bb": None, | |
939 | # non-tuple only |
|
994 | # non-tuple only | |
940 |
|
|
995 | "cc": None, | |
941 | # numeric tuple only |
|
996 | # numeric tuple only | |
942 |
(77, |
|
997 | (77, "x"): None, | |
943 | # numeric tuple and non-tuple |
|
998 | # numeric tuple and non-tuple | |
944 |
(88, |
|
999 | (88, "y"): None, | |
945 | 88: None, |
|
1000 | 88: None, | |
946 | # numeric non-tuple only |
|
1001 | # numeric non-tuple only | |
947 | 99: None, |
|
1002 | 99: None, | |
@@ -1133,12 +1188,12 b' class TestCompleter(unittest.TestCase):' | |||||
1133 | complete = ip.Completer.complete |
|
1188 | complete = ip.Completer.complete | |
1134 |
|
1189 | |||
1135 | ip.user_ns["d"] = { |
|
1190 | ip.user_ns["d"] = { | |
1136 |
0x |
|
1191 | 0xDEADBEEF: None, # 3735928559 | |
1137 | 1111: None, |
|
1192 | 1111: None, | |
1138 | 1234: None, |
|
1193 | 1234: None, | |
1139 | "1999": None, |
|
1194 | "1999": None, | |
1140 |
0b10101: None, |
|
1195 | 0b10101: None, # 21 | |
1141 | 22: None |
|
1196 | 22: None, | |
1142 | } |
|
1197 | } | |
1143 | _, matches = complete(line_buffer="d[1") |
|
1198 | _, matches = complete(line_buffer="d[1") | |
1144 | self.assertIn("1111", matches) |
|
1199 | self.assertIn("1111", matches) | |
@@ -1169,7 +1224,7 b' class TestCompleter(unittest.TestCase):' | |||||
1169 |
|
1224 | |||
1170 | ip.user_ns["C"] = C |
|
1225 | ip.user_ns["C"] = C | |
1171 | ip.user_ns["get"] = lambda: d |
|
1226 | ip.user_ns["get"] = lambda: d | |
1172 |
ip.user_ns["nested"] = { |
|
1227 | ip.user_ns["nested"] = {"x": d} | |
1173 |
|
1228 | |||
1174 | def assert_no_completion(**kwargs): |
|
1229 | def assert_no_completion(**kwargs): | |
1175 | _, matches = complete(**kwargs) |
|
1230 | _, matches = complete(**kwargs) | |
@@ -1198,7 +1253,7 b' class TestCompleter(unittest.TestCase):' | |||||
1198 | # nested dict completion |
|
1253 | # nested dict completion | |
1199 | assert_completion(line_buffer="nested['x'][") |
|
1254 | assert_completion(line_buffer="nested['x'][") | |
1200 |
|
1255 | |||
1201 |
with evaluation_level( |
|
1256 | with evaluation_level("minimal"): | |
1202 | with pytest.raises(AssertionError): |
|
1257 | with pytest.raises(AssertionError): | |
1203 | assert_completion(line_buffer="nested['x'][") |
|
1258 | assert_completion(line_buffer="nested['x'][") | |
1204 |
|
1259 | |||
@@ -1289,6 +1344,7 b' class TestCompleter(unittest.TestCase):' | |||||
1289 | _, matches = complete(line_buffer="d['") |
|
1344 | _, matches = complete(line_buffer="d['") | |
1290 | self.assertIn("my_head", matches) |
|
1345 | self.assertIn("my_head", matches) | |
1291 | self.assertIn("my_data", matches) |
|
1346 | self.assertIn("my_data", matches) | |
|
1347 | ||||
1292 | def completes_on_nested(): |
|
1348 | def completes_on_nested(): | |
1293 | ip.user_ns["d"] = numpy.zeros(2, dtype=dt) |
|
1349 | ip.user_ns["d"] = numpy.zeros(2, dtype=dt) | |
1294 | _, matches = complete(line_buffer="d[1]['my_head']['") |
|
1350 | _, matches = complete(line_buffer="d[1]['my_head']['") | |
@@ -1298,10 +1354,10 b' class TestCompleter(unittest.TestCase):' | |||||
1298 | with greedy_completion(): |
|
1354 | with greedy_completion(): | |
1299 | completes_on_nested() |
|
1355 | completes_on_nested() | |
1300 |
|
1356 | |||
1301 |
with evaluation_level( |
|
1357 | with evaluation_level("limitted"): | |
1302 | completes_on_nested() |
|
1358 | completes_on_nested() | |
1303 |
|
1359 | |||
1304 |
with evaluation_level( |
|
1360 | with evaluation_level("minimal"): | |
1305 | with pytest.raises(AssertionError): |
|
1361 | with pytest.raises(AssertionError): | |
1306 | completes_on_nested() |
|
1362 | completes_on_nested() | |
1307 |
|
1363 | |||
@@ -1653,32 +1709,32 b' class TestCompleter(unittest.TestCase):' | |||||
1653 |
|
1709 | |||
1654 |
|
1710 | |||
1655 | @pytest.mark.parametrize( |
|
1711 | @pytest.mark.parametrize( | |
1656 |
|
|
1712 | "input, expected", | |
1657 | [ |
|
1713 | [ | |
1658 |
[ |
|
1714 | ["1.234", "1.234"], | |
1659 | # should match signed numbers |
|
1715 | # should match signed numbers | |
1660 |
[ |
|
1716 | ["+1", "+1"], | |
1661 |
[ |
|
1717 | ["-1", "-1"], | |
1662 |
[ |
|
1718 | ["-1.0", "-1.0"], | |
1663 |
[ |
|
1719 | ["-1.", "-1."], | |
1664 |
[ |
|
1720 | ["+1.", "+1."], | |
1665 |
[ |
|
1721 | [".1", ".1"], | |
1666 | # should not match non-numbers |
|
1722 | # should not match non-numbers | |
1667 |
[ |
|
1723 | ["1..", None], | |
1668 |
[ |
|
1724 | ["..", None], | |
1669 |
[ |
|
1725 | [".1.", None], | |
1670 | # should match after comma |
|
1726 | # should match after comma | |
1671 |
[ |
|
1727 | [",1", "1"], | |
1672 |
[ |
|
1728 | [", 1", "1"], | |
1673 |
[ |
|
1729 | [", .1", ".1"], | |
1674 |
[ |
|
1730 | [", +.1", "+.1"], | |
1675 | # should not match after trailing spaces |
|
1731 | # should not match after trailing spaces | |
1676 |
[ |
|
1732 | [".1 ", None], | |
1677 | # some complex cases |
|
1733 | # some complex cases | |
1678 |
[ |
|
1734 | ["0b_0011_1111_0100_1110", "0b_0011_1111_0100_1110"], | |
1679 |
[ |
|
1735 | ["0xdeadbeef", "0xdeadbeef"], | |
1680 |
[ |
|
1736 | ["0b_1110_0101", "0b_1110_0101"], | |
1681 | ] |
|
1737 | ], | |
1682 | ) |
|
1738 | ) | |
1683 | def test_match_numeric_literal_for_dict_key(input, expected): |
|
1739 | def test_match_numeric_literal_for_dict_key(input, expected): | |
1684 | assert _match_number_in_dict_key_prefix(input) == expected |
|
1740 | assert _match_number_in_dict_key_prefix(input) == expected |
@@ -1,53 +1,54 b'' | |||||
1 | from typing import NamedTuple |
|
1 | from typing import NamedTuple | |
2 | from IPython.core.guarded_eval import EvaluationContext, GuardRejection, guarded_eval, unbind_method |
|
2 | from IPython.core.guarded_eval import ( | |
|
3 | EvaluationContext, | |||
|
4 | GuardRejection, | |||
|
5 | guarded_eval, | |||
|
6 | unbind_method, | |||
|
7 | ) | |||
3 | from IPython.testing import decorators as dec |
|
8 | from IPython.testing import decorators as dec | |
4 | import pytest |
|
9 | import pytest | |
5 |
|
10 | |||
6 |
|
11 | |||
7 | def limitted(**kwargs): |
|
12 | def limitted(**kwargs): | |
8 | return EvaluationContext( |
|
13 | return EvaluationContext(locals_=kwargs, globals_={}, evaluation="limitted") | |
9 | locals_=kwargs, |
|
|||
10 | globals_={}, |
|
|||
11 | evaluation='limitted' |
|
|||
12 | ) |
|
|||
13 |
|
14 | |||
14 |
|
15 | |||
15 | def unsafe(**kwargs): |
|
16 | def unsafe(**kwargs): | |
16 | return EvaluationContext( |
|
17 | return EvaluationContext(locals_=kwargs, globals_={}, evaluation="unsafe") | |
17 | locals_=kwargs, |
|
18 | ||
18 | globals_={}, |
|
|||
19 | evaluation='unsafe' |
|
|||
20 | ) |
|
|||
21 |
|
19 | |||
22 |
@dec.skip_without( |
|
20 | @dec.skip_without("pandas") | |
23 | def test_pandas_series_iloc(): |
|
21 | def test_pandas_series_iloc(): | |
24 | import pandas as pd |
|
22 | import pandas as pd | |
25 | series = pd.Series([1], index=['a']) |
|
23 | ||
|
24 | series = pd.Series([1], index=["a"]) | |||
26 | context = limitted(data=series) |
|
25 | context = limitted(data=series) | |
27 |
assert guarded_eval( |
|
26 | assert guarded_eval("data.iloc[0]", context) == 1 | |
28 |
|
27 | |||
29 |
|
28 | |||
30 |
@dec.skip_without( |
|
29 | @dec.skip_without("pandas") | |
31 | def test_pandas_series(): |
|
30 | def test_pandas_series(): | |
32 | import pandas as pd |
|
31 | import pandas as pd | |
33 | context = limitted(data=pd.Series([1], index=['a'])) |
|
32 | ||
|
33 | context = limitted(data=pd.Series([1], index=["a"])) | |||
34 | assert guarded_eval('data["a"]', context) == 1 |
|
34 | assert guarded_eval('data["a"]', context) == 1 | |
35 | with pytest.raises(KeyError): |
|
35 | with pytest.raises(KeyError): | |
36 | guarded_eval('data["c"]', context) |
|
36 | guarded_eval('data["c"]', context) | |
37 |
|
37 | |||
38 |
|
38 | |||
39 |
@dec.skip_without( |
|
39 | @dec.skip_without("pandas") | |
40 | def test_pandas_bad_series(): |
|
40 | def test_pandas_bad_series(): | |
41 | import pandas as pd |
|
41 | import pandas as pd | |
|
42 | ||||
42 | class BadItemSeries(pd.Series): |
|
43 | class BadItemSeries(pd.Series): | |
43 | def __getitem__(self, key): |
|
44 | def __getitem__(self, key): | |
44 |
return |
|
45 | return "CUSTOM_ITEM" | |
45 |
|
46 | |||
46 | class BadAttrSeries(pd.Series): |
|
47 | class BadAttrSeries(pd.Series): | |
47 | def __getattr__(self, key): |
|
48 | def __getattr__(self, key): | |
48 |
return |
|
49 | return "CUSTOM_ATTR" | |
49 |
|
50 | |||
50 |
bad_series = BadItemSeries([1], index=[ |
|
51 | bad_series = BadItemSeries([1], index=["a"]) | |
51 | context = limitted(data=bad_series) |
|
52 | context = limitted(data=bad_series) | |
52 |
|
53 | |||
53 | with pytest.raises(GuardRejection): |
|
54 | with pytest.raises(GuardRejection): | |
@@ -58,121 +59,108 b' def test_pandas_bad_series():' | |||||
58 | # note: here result is a bit unexpected because |
|
59 | # note: here result is a bit unexpected because | |
59 | # pandas `__getattr__` calls `__getitem__`; |
|
60 | # pandas `__getattr__` calls `__getitem__`; | |
60 | # FIXME - special case to handle it? |
|
61 | # FIXME - special case to handle it? | |
61 |
assert guarded_eval( |
|
62 | assert guarded_eval("data.a", context) == "CUSTOM_ITEM" | |
62 |
|
63 | |||
63 | context = unsafe(data=bad_series) |
|
64 | context = unsafe(data=bad_series) | |
64 |
assert guarded_eval('data["a"]', context) == |
|
65 | assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM" | |
65 |
|
66 | |||
66 |
bad_attr_series = BadAttrSeries([1], index=[ |
|
67 | bad_attr_series = BadAttrSeries([1], index=["a"]) | |
67 | context = limitted(data=bad_attr_series) |
|
68 | context = limitted(data=bad_attr_series) | |
68 | assert guarded_eval('data["a"]', context) == 1 |
|
69 | assert guarded_eval('data["a"]', context) == 1 | |
69 | with pytest.raises(GuardRejection): |
|
70 | with pytest.raises(GuardRejection): | |
70 |
guarded_eval( |
|
71 | guarded_eval("data.a", context) | |
71 |
|
72 | |||
72 |
|
73 | |||
73 |
@dec.skip_without( |
|
74 | @dec.skip_without("pandas") | |
74 | def test_pandas_dataframe_loc(): |
|
75 | def test_pandas_dataframe_loc(): | |
75 | import pandas as pd |
|
76 | import pandas as pd | |
76 | from pandas.testing import assert_series_equal |
|
77 | from pandas.testing import assert_series_equal | |
77 | data = pd.DataFrame([{'a': 1}]) |
|
78 | ||
|
79 | data = pd.DataFrame([{"a": 1}]) | |||
78 | context = limitted(data=data) |
|
80 | context = limitted(data=data) | |
79 | assert_series_equal( |
|
81 | assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"]) | |
80 | guarded_eval('data.loc[:, "a"]', context), |
|
|||
81 | data['a'] |
|
|||
82 | ) |
|
|||
83 |
|
82 | |||
84 |
|
83 | |||
85 | def test_named_tuple(): |
|
84 | def test_named_tuple(): | |
86 |
|
||||
87 | class GoodNamedTuple(NamedTuple): |
|
85 | class GoodNamedTuple(NamedTuple): | |
88 | a: str |
|
86 | a: str | |
89 | pass |
|
87 | pass | |
90 |
|
88 | |||
91 | class BadNamedTuple(NamedTuple): |
|
89 | class BadNamedTuple(NamedTuple): | |
92 | a: str |
|
90 | a: str | |
|
91 | ||||
93 | def __getitem__(self, key): |
|
92 | def __getitem__(self, key): | |
94 | return None |
|
93 | return None | |
95 |
|
94 | |||
96 |
good = GoodNamedTuple(a= |
|
95 | good = GoodNamedTuple(a="x") | |
97 |
bad = BadNamedTuple(a= |
|
96 | bad = BadNamedTuple(a="x") | |
98 |
|
97 | |||
99 | context = limitted(data=good) |
|
98 | context = limitted(data=good) | |
100 |
assert guarded_eval( |
|
99 | assert guarded_eval("data[0]", context) == "x" | |
101 |
|
100 | |||
102 | context = limitted(data=bad) |
|
101 | context = limitted(data=bad) | |
103 | with pytest.raises(GuardRejection): |
|
102 | with pytest.raises(GuardRejection): | |
104 |
guarded_eval( |
|
103 | guarded_eval("data[0]", context) | |
105 |
|
104 | |||
106 |
|
105 | |||
107 | def test_dict(): |
|
106 | def test_dict(): | |
108 | context = limitted( |
|
107 | context = limitted(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3}) | |
109 | data={'a': 1, 'b': {'x': 2}, ('x', 'y'): 3} |
|
|||
110 | ) |
|
|||
111 | assert guarded_eval('data["a"]', context) == 1 |
|
108 | assert guarded_eval('data["a"]', context) == 1 | |
112 |
assert guarded_eval('data["b"]', context) == { |
|
109 | assert guarded_eval('data["b"]', context) == {"x": 2} | |
113 | assert guarded_eval('data["b"]["x"]', context) == 2 |
|
110 | assert guarded_eval('data["b"]["x"]', context) == 2 | |
114 | assert guarded_eval('data["x", "y"]', context) == 3 |
|
111 | assert guarded_eval('data["x", "y"]', context) == 3 | |
115 |
|
112 | |||
116 |
assert guarded_eval( |
|
113 | assert guarded_eval("data.keys", context) | |
117 |
|
114 | |||
118 |
|
115 | |||
119 | def test_set(): |
|
116 | def test_set(): | |
120 |
context = limitted(data={ |
|
117 | context = limitted(data={"a", "b"}) | |
121 |
assert guarded_eval( |
|
118 | assert guarded_eval("data.difference", context) | |
122 |
|
119 | |||
123 |
|
120 | |||
124 | def test_list(): |
|
121 | def test_list(): | |
125 | context = limitted(data=[1, 2, 3]) |
|
122 | context = limitted(data=[1, 2, 3]) | |
126 |
assert guarded_eval( |
|
123 | assert guarded_eval("data[1]", context) == 2 | |
127 |
assert guarded_eval( |
|
124 | assert guarded_eval("data.copy", context) | |
128 |
|
125 | |||
129 |
|
126 | |||
130 | def test_dict_literal(): |
|
127 | def test_dict_literal(): | |
131 | context = limitted() |
|
128 | context = limitted() | |
132 |
assert guarded_eval( |
|
129 | assert guarded_eval("{}", context) == {} | |
133 | assert guarded_eval('{"a": 1}', context) == {"a": 1} |
|
130 | assert guarded_eval('{"a": 1}', context) == {"a": 1} | |
134 |
|
131 | |||
135 |
|
132 | |||
136 | def test_list_literal(): |
|
133 | def test_list_literal(): | |
137 | context = limitted() |
|
134 | context = limitted() | |
138 |
assert guarded_eval( |
|
135 | assert guarded_eval("[]", context) == [] | |
139 | assert guarded_eval('[1, "a"]', context) == [1, "a"] |
|
136 | assert guarded_eval('[1, "a"]', context) == [1, "a"] | |
140 |
|
137 | |||
141 |
|
138 | |||
142 | def test_set_literal(): |
|
139 | def test_set_literal(): | |
143 | context = limitted() |
|
140 | context = limitted() | |
144 |
assert guarded_eval( |
|
141 | assert guarded_eval("set()", context) == set() | |
145 | assert guarded_eval('{"a"}', context) == {"a"} |
|
142 | assert guarded_eval('{"a"}', context) == {"a"} | |
146 |
|
143 | |||
147 |
|
144 | |||
148 | def test_if_expression(): |
|
145 | def test_if_expression(): | |
149 | context = limitted() |
|
146 | context = limitted() | |
150 |
assert guarded_eval( |
|
147 | assert guarded_eval("2 if True else 3", context) == 2 | |
151 |
assert guarded_eval( |
|
148 | assert guarded_eval("4 if False else 5", context) == 5 | |
152 |
|
149 | |||
153 |
|
150 | |||
154 | def test_object(): |
|
151 | def test_object(): | |
155 | obj = object() |
|
152 | obj = object() | |
156 | context = limitted(obj=obj) |
|
153 | context = limitted(obj=obj) | |
157 |
assert guarded_eval( |
|
154 | assert guarded_eval("obj.__dir__", context) == obj.__dir__ | |
158 |
|
155 | |||
159 |
|
156 | |||
160 | @pytest.mark.parametrize( |
|
157 | @pytest.mark.parametrize( | |
161 | "code,expected", |
|
158 | "code,expected", | |
162 | [ |
|
159 | [ | |
163 | [ |
|
160 | ["int.numerator", int.numerator], | |
164 | 'int.numerator', |
|
161 | ["float.is_integer", float.is_integer], | |
165 | int.numerator |
|
162 | ["complex.real", complex.real], | |
166 |
|
|
163 | ], | |
167 | [ |
|
|||
168 | 'float.is_integer', |
|
|||
169 | float.is_integer |
|
|||
170 | ], |
|
|||
171 | [ |
|
|||
172 | 'complex.real', |
|
|||
173 | complex.real |
|
|||
174 | ] |
|
|||
175 | ] |
|
|||
176 | ) |
|
164 | ) | |
177 | def test_number_attributes(code, expected): |
|
165 | def test_number_attributes(code, expected): | |
178 | assert guarded_eval(code, limitted()) == expected |
|
166 | assert guarded_eval(code, limitted()) == expected | |
@@ -180,25 +168,15 b' def test_number_attributes(code, expected):' | |||||
180 |
|
168 | |||
181 | def test_method_descriptor(): |
|
169 | def test_method_descriptor(): | |
182 | context = limitted() |
|
170 | context = limitted() | |
183 |
assert guarded_eval( |
|
171 | assert guarded_eval("list.copy.__name__", context) == "copy" | |
184 |
|
172 | |||
185 |
|
173 | |||
186 | @pytest.mark.parametrize( |
|
174 | @pytest.mark.parametrize( | |
187 | "data,good,bad,expected", |
|
175 | "data,good,bad,expected", | |
188 | [ |
|
176 | [ | |
189 | [ |
|
177 | [[1, 2, 3], "data.index(2)", "data.append(4)", 1], | |
190 | [1, 2, 3], |
|
178 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], | |
191 | 'data.index(2)', |
|
179 | ], | |
192 | 'data.append(4)', |
|
|||
193 | 1 |
|
|||
194 | ], |
|
|||
195 | [ |
|
|||
196 | {'a': 1}, |
|
|||
197 | 'data.keys().isdisjoint({})', |
|
|||
198 | 'data.update()', |
|
|||
199 | True |
|
|||
200 | ] |
|
|||
201 | ] |
|
|||
202 | ) |
|
180 | ) | |
203 | def test_calls(data, good, bad, expected): |
|
181 | def test_calls(data, good, bad, expected): | |
204 | context = limitted(data=data) |
|
182 | context = limitted(data=data) | |
@@ -211,19 +189,10 b' def test_calls(data, good, bad, expected):' | |||||
211 | @pytest.mark.parametrize( |
|
189 | @pytest.mark.parametrize( | |
212 | "code,expected", |
|
190 | "code,expected", | |
213 | [ |
|
191 | [ | |
214 | [ |
|
192 | ["(1\n+\n1)", 2], | |
215 | '(1\n+\n1)', |
|
193 | ["list(range(10))[-1:]", [9]], | |
216 | 2 |
|
194 | ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], | |
217 |
|
|
195 | ], | |
218 | [ |
|
|||
219 | 'list(range(10))[-1:]', |
|
|||
220 | [9] |
|
|||
221 | ], |
|
|||
222 | [ |
|
|||
223 | 'list(range(20))[3:-2:3]', |
|
|||
224 | [3, 6, 9, 12, 15] |
|
|||
225 | ] |
|
|||
226 | ] |
|
|||
227 | ) |
|
196 | ) | |
228 | def test_literals(code, expected): |
|
197 | def test_literals(code, expected): | |
229 | context = limitted() |
|
198 | context = limitted() | |
@@ -232,22 +201,20 b' def test_literals(code, expected):' | |||||
232 |
|
201 | |||
233 | def test_subscript(): |
|
202 | def test_subscript(): | |
234 | context = EvaluationContext( |
|
203 | context = EvaluationContext( | |
235 | locals_={}, |
|
204 | locals_={}, globals_={}, evaluation="limitted", in_subscript=True | |
236 | globals_={}, |
|
|||
237 | evaluation='limitted', |
|
|||
238 | in_subscript=True |
|
|||
239 | ) |
|
205 | ) | |
240 | empty_slice = slice(None, None, None) |
|
206 | empty_slice = slice(None, None, None) | |
241 |
assert guarded_eval( |
|
207 | assert guarded_eval("", context) == tuple() | |
242 |
assert guarded_eval( |
|
208 | assert guarded_eval(":", context) == empty_slice | |
243 |
assert guarded_eval( |
|
209 | assert guarded_eval("1:2:3", context) == slice(1, 2, 3) | |
244 | assert guarded_eval(':, "a"', context) == (empty_slice, "a") |
|
210 | assert guarded_eval(':, "a"', context) == (empty_slice, "a") | |
245 |
|
211 | |||
246 |
|
212 | |||
247 | def test_unbind_method(): |
|
213 | def test_unbind_method(): | |
248 | class X(list): |
|
214 | class X(list): | |
249 | def index(self, k): |
|
215 | def index(self, k): | |
250 |
return |
|
216 | return "CUSTOM" | |
|
217 | ||||
251 | x = X() |
|
218 | x = X() | |
252 | assert unbind_method(x.index) is X.index |
|
219 | assert unbind_method(x.index) is X.index | |
253 | assert unbind_method([].index) is list.index |
|
220 | assert unbind_method([].index) is list.index | |
@@ -261,16 +228,19 b' def test_assumption_instance_attr_do_not_matter():' | |||||
261 | versions could invalidate this assumptions. This test |
|
228 | versions could invalidate this assumptions. This test | |
262 | is meant to catch such a change if it ever comes true. |
|
229 | is meant to catch such a change if it ever comes true. | |
263 | """ |
|
230 | """ | |
|
231 | ||||
264 | class T: |
|
232 | class T: | |
265 | def __getitem__(self, k): |
|
233 | def __getitem__(self, k): | |
266 |
return |
|
234 | return "a" | |
|
235 | ||||
267 | def __getattr__(self, k): |
|
236 | def __getattr__(self, k): | |
268 |
return |
|
237 | return "a" | |
|
238 | ||||
269 | t = T() |
|
239 | t = T() | |
270 |
t.__getitem__ = lambda f: |
|
240 | t.__getitem__ = lambda f: "b" | |
271 |
t.__getattr__ = lambda f: |
|
241 | t.__getattr__ = lambda f: "b" | |
272 |
assert t[1] == |
|
242 | assert t[1] == "a" | |
273 |
assert t[1] == |
|
243 | assert t[1] == "a" | |
274 |
|
244 | |||
275 |
|
245 | |||
276 | def test_assumption_named_tuples_share_getitem(): |
|
246 | def test_assumption_named_tuples_share_getitem(): |
General Comments 0
You need to be logged in to leave comments.
Login now